view release on metacpan or search on metacpan
- [infix] Refined the Windows x64 ABI to pass all vector types by reference (pointer in GPR). This ensures compatibility with MSVC which expects even 128-bit vectors to be passed via pointer in many scenarios, while still returning them by value in `...
- [infix] Move to a pre-calculated hash field in `_infix_registry_entry_t`. Lookups and rehashing now use this stored hash, significantly reducing string hashing overhead during type resolution and registry scaling.
- [infix] Optimized Type Registry memory management: Internal hash table buckets are now heap-allocated and freed during rehashes, preventing memory "leaks" within the registry's arena.
## [v1.0.6] - 2026-01-22
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] 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.
- \[infix] Fixed a logic error in the System V reverse argument classifier where vectors were defaulting to `INTEGER` class, causing the trampoline to look in `RDI`/`RSI` instead of `XMM` registers.
## [v1.0.2] - 2025-12-14
### Changed
- In an attempt to debug mystery failures in SDL3.pm, Affix.pm will warn and return `undef` instead of `croak`ing.
- Improved error reporting: if the internal error message is empty, the numeric error code is now included in the warning.
### Fixed
- [[infix]] Fixed a critical file descriptor leak on POSIX platforms (Linux/FreeBSD) where the file descriptor returned by `shm_open` was kept open for the lifetime of the trampoline, eventually hitting the process file descriptor limit (EMFILE). T...
- Fixed memory leaks that occurred when trampoline creation failed midway (cleaning up partial arenas, strings, and backend structures).
## [v1.0.1] - 2025-12-13
### Changed
- Improved Union marshalling: Union members are now exposed as pins within the hash. This allows clean syntax (like `$u->{member} = 5`) without needing to dereference a reference, while maintaining C-memory aliasing.
### Fixed
$errno = 0; # Writes directly to C memory
```
## `unpin( $var )`
Removes the magic applied by `pin`. The variable retains its last value but is no longer linked to C memory.
# TYPE SYSTEM
Affix signatures are built using helper functions that map precisely to C types. These are exported by default, or can
be imported explicitly using the `:types` tag.
## Primitive Types
### Void & Booleans
- `Void`: Used for functions that return nothing (`void`).
- `Bool`: Mapped to Perl's true/false values (`stdbool.h` / `_Bool`).
### Characters
- `Char`: Standard signed `char` (usually 8-bit).
- `SChar`: Explicitly signed `signed char`.
- `UChar`: Unsigned `unsigned char`.
- `WChar`: Wide character (`wchar_t`), usually 16-bit on Windows and 32-bit on Linux/macOS.
- `Char8`, `Char16`, `Char32`: Explicit-width C++ character types (`char8_t`, etc.).
### Platform-Native Integers
These types map to the system's native bit-widths (e.g., `Long` is 32-bit on Windows x64, but 64-bit on Linux x64).
- `Short` / `UShort`: `short` / `unsigned short`.
- `Int` / `UInt`: `int` / `unsigned int` (typically 32-bit).
- `Long` / `ULong`: `long` / `unsigned long`.
- `LongLong` / `ULongLong`: `long long` / `unsigned long long` (guaranteed at least 64-bit).
- `Size_t` / `SSize_t`: Standard memory and array indexing types (`size_t`, `ssize_t`).
### Fixed-Width Integers
Use these when a C library explicitly requests a `stdint.h` type.
### Floating Point
- `Float16`: Half-precision 16-bit float (IEEE 754).
- `Float` / `Float32`: Standard 32-bit `float`.
- `Double` / `Float64`: Standard 64-bit `double`.
- `LongDouble`: Platform-specific extended precision (typically 80-bit on x86 or 128-bit).
### Complex Numbers
- `Complex[ $type ]`: C99 complex numbers (e.g., `Complex[Double]`). In Perl, these map to an `ArrayRef` of two numbers: `[ $real, $imaginary ]`.
## String Types
- **`String`**: Maps to `const char*`. Affix handles UTF-8 encoding (Perl to C) and decoding (C to Perl) automatically.
- **`WString`**: Maps to `const wchar_t*`. Affix automatically handles UTF-16/UTF-32 conversions, including Windows Surrogate Pairs.
- **`StringList`**: Maps a Perl `ArrayRef` of strings to a null-terminated `char**` array (common in C APIs like `execve` or `main(argc, argv)`).
- **`Buffer`**: Maps a mutable `char*` to the raw memory buffer of a Perl scalar. **Zero-copy**. The scalar must have pre-allocated capacity (e.g., `"\0" x 1024`).
## Pointer & Reference Types
### Specialized Pointers
- **`File`** / **`PerlIO`**: Maps Perl filehandles (Globs or IO objects) to `FILE*` or `PerlIO*`. **Must** be wrapped in a pointer: `Pointer[File]`.
- **`SockAddr`**: Specialized marshalling for packed socket strings (e.g., from `Socket::pack_sockaddr_in`) to `struct sockaddr*`.
- **`SV`**: Direct, low-level access to Perl's internal Interpreter Object (`SV*`). **Must** be wrapped in a pointer: `Pointer[SV]`.
## Aggregate Types
### `Struct[ @members ]`
A C struct, mapped to a Perl `HashRef`.
```perl
# C: typedef struct { int x; int y; } Point;
typedef Point => Struct[ x => Int, y => Int ];
```
### `Union[ @members ]`
A C union, mapped to a Perl `HashRef` with exactly one key.
```perl
# C: union { int key_code; float pressure; };
typedef Event => Union[ key_code => Int, pressure => Float ];
```
### `Packed[ $align, $aggregate ]`
Forces specific byte alignment on a Struct or Union (e.g., `#pragma pack(1)`).
## Assembly
When writing raw Assembly (NASM/GAS), you must manually adhere to the calling convention of your target platform:
- **Linux/macOS (System V AMD64 ABI):** Arguments are passed in `rdi, rsi, rdx, rcx, r8, r9`, with the rest on the stack.
- **Windows (Microsoft x64):** Arguments are passed in `rcx, rdx, r8, r9`, with "shadow space" reserved on the stack.
## Go
Go libraries can be loaded if they are compiled with `-buildmode=c-shared`. Note that Go slices and strings contain
internal metadata (length/capacity) and do not map directly to C arrays or `char*`. Use the `C` package inside Go
(`import "C"`) and `*C.char` to bridge the boundary.
# ERROR HANDLING & DEBUGGING
Bridging two entirely different runtimes can lead to spectacular crashes if types or memory boundaries are mismatched.
Affix provides built-in tools to help you identify what went wrong.
## Error Handling
### `errno()`
builder/Affix/Builder.pm view on Meta::CPAN
if ( !$is_win ) {
$cflags .= ' -pthread';
$ldflags .= ' -pthread';
}
}
method write_file( $filename, $content ) { path($filename)->spew_raw($content) or die "Could not open $filename: $!\n" }
method read_file ($filename) { path($filename)->slurp_utf8 or die "Could not open $filename: $!\n" }
method step_build() {
$self->step_affix;
my %modules = map { $_ => catfile( 'blib', $_ ) } find( qr/\.pm$/, 'lib' );
my %docs = map { $_ => catfile( 'blib', $_ ) } find( qr/\.pod$/, 'lib' );
my %scripts = map { $_ => catfile( 'blib', $_ ) } find( qr/(?:)/, 'script' );
my %sdocs = map { $_ => delete $scripts{$_} } grep {/.pod$/} keys %scripts;
my %dist_shared = map { $_ => catfile( qw[blib lib auto share dist], $meta->name, abs2rel( $_, 'share' ) ) } find( qr/(?:)/, 'share' );
my %module_shared = map { $_ => catfile( qw[blib lib auto share module], abs2rel( $_, 'module-share' ) ) } find( qr/(?:)/, 'module-share' );
pm_to_blib( { %modules, %docs, %scripts, %dist_shared, %module_shared }, catdir(qw[blib lib auto]) );
make_executable($_) for values %scripts;
make_path( catdir(qw[blib arch]), { chmod => 0777, verbose => $verbose } );
0;
}
method step_clean() { remove_tree( $_, { verbose => $verbose } ) for qw[blib temp]; 0 }
method step_install() {
$self->step_build() unless -d 'blib';
my %res;
install(
[ from_to => $install_paths->install_map,
verbose => $verbose,
always_copy => 1,
dry_run => $dry_run,
uninst => $uninst,
result => \%res
]
);
# In the future, I might check the values of %res according to https://metacpan.org/pod/ExtUtils::Install#install
0;
}
method step_realclean () { remove_tree( $_, { verbose => $verbose } ) for qw[blib temp Build _build_params MYMETA.yml MYMETA.json]; 0 }
method step_test() {
$self->step_build() unless -d 'blib';
require TAP::Harness::Env;
my %test_args = (
( verbosity => $verbose ),
( jobs => $jobs ),
( color => -t STDOUT ),
lib => [ map { rel2abs( catdir( 'blib', $_ ) ) } qw[arch lib] ],
);
TAP::Harness::Env->create( \%test_args )->runtests( sort map { $_->stringify } find( qr/\.t$/, 't' ) )->has_errors;
}
method get_arguments (@sources) {
$_ = detildefy($_) for grep {defined} $install_base, $destdir, $prefix, values %{$install_paths};
$install_paths = ExtUtils::InstallPaths->new( dist_name => $meta->name );
return;
}
method Build(@args) {
my $method = $self->can( 'step_' . $action );
builder/Affix/Builder.pm view on Meta::CPAN
}
method Build_PL() {
die "Pure perl Affix? Ha! You wish.\n" if $pureperl;
say sprintf 'Creating new Build script for %s %s', $meta->name, $meta->version;
$self->write_file( 'Build', sprintf <<'', $^X, __PACKAGE__, __PACKAGE__ );
#!%s
use lib 'builder';
use %s;
%s->new( @ARGV && $ARGV[0] =~ /\A\w+\z/ ? ( action => shift @ARGV ) : (),
map { /^--/ ? ( shift(@ARGV) =~ s[^--][]r => 1 ) : /^-/ ? ( shift(@ARGV) =~ s[^-][]r => shift @ARGV ) : () } @ARGV )->Build();
make_executable('Build');
my @env = defined $ENV{PERL_MB_OPT} ? split_like_shell( $ENV{PERL_MB_OPT} ) : ();
$self->write_file( '_build_params', encode_json( [ \@env, \@ARGV ] ) );
if ( my $dynamic = $meta->custom('x_dynamic_prereqs') ) {
my %meta = ( %{ $meta->as_struct }, dynamic_config => 1 );
$self->get_arguments( \@env, \@ARGV );
require CPAN::Requirements::Dynamic;
my $dynamic_parser = CPAN::Requirements::Dynamic->new();
my $prereq = $dynamic_parser->evaluate($dynamic);
builder/Affix/Builder.pm view on Meta::CPAN
# Detect Compiler settings using Perl's Config as base
my $cc_cmd = $Config{cc} || 'cc';
my $cc_type = 'gcc'; # Default flavor
if ( $cc_cmd =~ /cl(\.exe)?$/i ) { $cc_type = 'msvc'; }
elsif ( $cc_cmd =~ /clang/i ) { $cc_type = 'clang'; }
elsif ( $cc_cmd =~ /gcc/i ) { $cc_type = 'gcc'; }
elsif ( $cc_cmd =~ /egcc/i ) { $cc_type = 'gcc'; }
# Setup Flags
my ( $ar_cmd, @cflags, @arflags, $out_flag_cc, $out_flag_ar );
my @includes = map { ( $cc_type eq 'msvc' ? '/I' : '-I' ) . $_ } @include_dirs;
if ( $cc_type eq 'msvc' ) {
$ar_cmd = 'lib';
@cflags = ( '/nologo', '/c', '/std:c11', '/W3', '/GS', '/MD', '/O2', @includes );
@cflags = ( @cflags, '/DINFIX_DEBUG_ENABLED=1' ) if $verbose;
@arflags = ('/nologo');
$out_flag_cc = '/Fo';
$out_flag_ar = '/OUT:';
}
else {
# GCC / Clang
infix/src/arch/x64/abi_x64_emitters.c view on Meta::CPAN
emit_byte(buf, byte2);
uint8_t byte3 = ((w & 1) << 7) | ((~v & 0xF) << 3) | ((l & 1) << 2) | (p & 3);
emit_byte(buf, byte3);
}
}
/**
* @internal
* @brief Emits a 4-byte EVEX prefix for an AVX-512 instruction, following the Intel SDM.
*/
INFIX_INTERNAL void emit_evex_prefix(code_buffer * buf,
uint8_t map, // 1 for 0F, 2 for 0F38, 3 for 0F3A
uint8_t pp, // 00=none, 01=66, 10=F3, 11=F2
bool W,
bool R,
bool X,
bool B,
bool R_prime, // Register bits
uint8_t vvvv, // Source register (inverted)
bool L,
bool L_prime,
bool z,
bool b,
uint8_t aaa) // Masking/control bits
{
emit_byte(buf, 0x62);
// Byte 2: P0 - R, X, B, R' bits are inverted. 0 means 1, 1 means 0.
uint8_t p0 = 0;
p0 |= (R ? 0 : 1) << 7; // Inverted R bit
p0 |= (X ? 0 : 1) << 6; // Inverted X bit
p0 |= (B ? 0 : 1) << 5; // Inverted B bit
p0 |= (R_prime ? 0 : 1) << 4; // Inverted R' bit
p0 |= (map & 0x0F); // Low 4 bits select the opcode map (0F, 0F38, 0F3A)
emit_byte(buf, p0);
// Byte 3: P1
uint8_t p1 = 0;
p1 |= (pp & 0b11);
p1 |= (1 << 2); // ' (marks EVEX), must be 1
p1 |= ((~vvvv & 0xF) << 3); // vvvv field is inverted
p1 |= W ? (1 << 7) : 0;
emit_byte(buf, p1);
// Byte 4: P2
uint8_t p2 = 0;
infix/src/arch/x64/abi_x64_emitters.c view on Meta::CPAN
p2 |= (((vvvv >> 4) & 1) << 3);
emit_byte(buf, p2);
}
/**
* @internal
* @brief Emits `vmovupd ymm, [base + offset]` to load a 256-bit unaligned value (AVX).
* @details Instruction format: VEX.256.66.0F.WIG 10 /r
*/
INFIX_INTERNAL void emit_vmovupd_ymm_mem(code_buffer * buf, x64_xmm dest, x64_gpr src_base, int32_t offset) {
// VEX prefix fields for vmovupd ymm, m256:
// L=1 (256-bit), p=1 (from 66 prefix), m-mmmm=01 (from 0F map).
// The vvvv field is not used for a memory source and should be 0.
emit_vex_prefix(buf, dest >= XMM8_REG, 0, src_base >= R8_REG, 1, false, 0, true, 1);
emit_byte(buf, 0x10); // Opcode for MOVUPD
uint8_t mod = (offset >= -128 && offset <= 127) ? 0x40 : 0x80;
if (offset == 0 && (src_base % 8) != RBP_REG)
mod = 0x00;
emit_modrm(buf, mod >> 6, dest % 8, src_base % 8);
if (src_base % 8 == RSP_REG)
emit_byte(buf, 0x24);
if (mod == 0x40)
infix/src/arch/x64/abi_x64_emitters.c view on Meta::CPAN
* @internal
* @brief Emits `vmovupd [base + offset], zmm` to store a 512-bit unaligned value (AVX-512).
* @details Instruction format: EVEX.512.66.0F.W0 11 /r
*/
INFIX_INTERNAL void emit_vmovupd_mem_zmm(code_buffer * buf, x64_gpr dest_base, int32_t offset, x64_xmm src) {
// For a store, the source register is encoded in EVEX.reg_field (via ModRM)
// and the vvvv field is repurposed. Per Intel SDM, for a memory destination,
// V' must be 1. We encode this by passing a value with the 5th bit set (16)
// to the vvvv parameter of the prefix emitter.
emit_evex_prefix(buf,
1, // map: 0F
1, // pp: 66
true, // W: 1 (double-precision)
src >= XMM8_REG,
false,
dest_base >= R8_REG,
src >= XMM16_REG,
16, // vvvv field + V' bit encoding for memory destination
false, // L=0 for 512-bit
true, // L'=1 for 512-bit
false, // z=0
infix/src/arch/x64/abi_x64_emitters.c view on Meta::CPAN
else if (mod == 0x80)
emit_int32(buf, offset);
}
/**
* @internal
* @brief Emits `vzeroupper` to clear the upper bits of all YMM/ZMM registers.
* @details Opcode format: VEX.128.0F.77
*/
INFIX_INTERNAL void emit_vzeroupper(code_buffer * buf) {
// VEX.128.0F.77 (C5 F8 77)
// L=0 (128-bit), p=0 (no prefix), m-mmmm=1 (0F map), vvvv=1111 (none)
emit_vex_prefix(buf, 0, 0, 0, 1, 0, 0, 0, 0);
emit_byte(buf, 0x77);
}
/**
* @internal
* @brief Emits `cvtsd2ss xmm1, xmm2/m64` to convert a double to a float.
* @details Opcode format: F2 0F 5A /r
*/
INFIX_INTERNAL void emit_cvtsd2ss_xmm_xmm(code_buffer * buf, x64_xmm dest, x64_xmm src) {
emit_byte(buf, 0xF2);
infix/src/common/compat_c23.h view on Meta::CPAN
* compilation environments. While `NULL` is standard in C, `nullptr` is preferred
* in modern C++ and is being adopted in newer C standards. This macro ensures
* the codebase can use `nullptr` consistently without causing compilation errors
* in a strict C11/C17 environment.
*/
#if !defined(nullptr) && !defined(__cplusplus)
#define nullptr ((void *)0)
#endif
/**
* @def static_assert
* @brief Defines a `static_assert` macro that maps to the C11 `_Static_assert`.
*
* @note This is a polyfill for older compilers or environments that might not
* define `static_assert` as a more convenient macro in `<assert.h>` by default,
* even if they support the underlying `_Static_assert` keyword. It allows for
* compile-time assertions throughout the codebase.
*/
#if !defined(__cplusplus) && \
((defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L) || (defined(INFIX_COMPILER_MSVC) && _MSC_VER >= 1900))
#include <assert.h>
#ifndef static_assert
infix/src/common/infix_internals.h view on Meta::CPAN
* @details This struct encapsulates the platform-specific details of allocating and
* managing executable memory in a way that is compliant with modern OS security
* features like W^X (Write XOR Execute). It supports two primary strategies:
*
* 1. **Single-Mapping W^X (Windows/macOS/Android):** A single memory region is
* allocated as Read-Write (`rw_ptr`). After the JIT compiler writes the
* machine code to this region, its permissions are changed to Read-Execute.
* In this model, `rx_ptr` and `rw_ptr` point to the same address.
*
* 2. **Dual-Mapping W^X (Linux/BSD):** A single underlying shared memory object
* is mapped into the process's address space twice: once as Read-Write
* (`rw_ptr`) and once as Read-Execute (`rx_ptr`). The pointers have different
* virtual addresses but point to the same physical memory. This is required
* on systems with stricter W^X enforcement.
*/
typedef struct {
#if defined(INFIX_OS_WINDOWS)
HANDLE handle; /**< The handle from `VirtualAlloc`, needed for `VirtualFree`. */
void * seh_registration; /**< (Windows x64) Opaque handle from `RtlAddFunctionTable`. */
#else
int shm_fd; /**< The file descriptor for shared memory on dual-mapping POSIX systems. -1 otherwise. */
void * eh_frame_ptr; /**< (POSIX) Pointer to the registered .eh_frame data. */
#endif
void * rx_ptr; /**< The read-execute memory address. This is the callable function pointer. */
void * rw_ptr; /**< The read-write memory address. The JIT compiler writes machine code here. */
size_t size; /**< The size of the allocated memory region in bytes. */
} infix_executable_t;
/**
* @struct infix_protected_t
* @brief Internal representation of a memory block that will be made read-only.
*
infix/src/common/infix_internals.h view on Meta::CPAN
* `g_infix_last_error` struct. For parser errors, it generates a rich diagnostic
* message with a code snippet.
* @param category The general category of the error.
* @param code The specific error code.
* @param position For parser errors, the byte offset into the signature string where the error occurred.
*/
INFIX_INTERNAL void _infix_set_error(infix_error_category_t category, infix_error_code_t code, size_t position);
/**
* @brief Sets the thread-local error state for a system-level error.
* @details Located in `src/core/error.c`, this is used for errors originating from
* the operating system, such as `dlopen` or `mmap` failures.
* @param category The general category of the error.
* @param code The `infix` error code that corresponds to the failure.
* @param system_code The OS-specific error code (e.g., from `errno` or `GetLastError`).
* @param msg An optional custom message from the OS (e.g., from `dlerror`).
*/
INFIX_INTERNAL void _infix_set_system_error(infix_error_category_t category,
infix_error_code_t code,
long system_code,
const char * msg);
/**
infix/src/common/infix_internals.h view on Meta::CPAN
INFIX_INTERNAL void emit_int32(code_buffer * buf, int32_t value);
/**
* @brief A convenience wrapper to append a 64-bit integer (little-endian) to a code buffer.
* @param[in,out] buf The code buffer.
* @param[in] value The 64-bit integer to append.
*/
INFIX_INTERNAL void emit_int64(code_buffer * buf, int64_t value);
/**
* @brief Allocates a block of executable memory using the platform's W^X strategy.
* @details Located in `src/jit/executor.c`. This is a platform-specific function
* that abstracts `VirtualAlloc`, `mmap` with `MAP_JIT`, or `shm_open` with dual-mapping.
* @param size The number of bytes to allocate.
* @return An `infix_executable_t` handle containing pointers to the allocated memory.
*/
INFIX_INTERNAL c23_nodiscard infix_executable_t infix_executable_alloc(size_t size);
/**
* @brief Frees a block of executable memory and applies guard pages to prevent use-after-free.
* @details Located in `src/jit/executor.c`. Before freeing, it attempts to change
* the memory's protection to be inaccessible, causing an immediate crash on a UAF.
* @param exec The handle to the memory block to free.
*/
INFIX_INTERNAL void infix_executable_free(infix_executable_t exec);
typedef enum {
INFIX_EXECUTABLE_FORWARD,
INFIX_EXECUTABLE_SAFE_FORWARD,
INFIX_EXECUTABLE_REVERSE,
INFIX_EXECUTABLE_DIRECT
} infix_executable_category_t;
/**
* @brief Makes a block of JIT memory executable, completing the W^X process.
* @details Located in `src/jit/executor.c`. For single-map platforms, this calls
* `VirtualProtect` or `mprotect`. For dual-map platforms, this is a no-op. It
* also handles instruction cache flushing on relevant architectures like AArch64.
* @param exec The handle to the memory block to make executable.
* @param prologue_size The size of the generated prologue in bytes.
* @param category The type of trampoline being finalized.
* @return `true` on success, `false` on failure.
*/
INFIX_INTERNAL c23_nodiscard bool infix_executable_make_executable(infix_executable_t * exec,
infix_executable_category_t category,
uint32_t prologue_size,
uint32_t epilogue_offset);
infix/src/core/error.c view on Meta::CPAN
*
* @details This is set by the high-level API functions (`infix_type_from_signature`, etc.)
* before parsing begins. If a parser error occurs, `_infix_set_error` uses this
* context to generate a rich, contextual error message.
*/
INFIX_TLS const char * g_infix_last_signature_context = nullptr;
/**
* @internal
* @brief Maps an `infix_error_code_t` to its human-readable string representation.
* @param code The error code to map.
* @return A constant string describing the error.
*/
static const char * _get_error_message_for_code(infix_error_code_t code) {
switch (code) {
case INFIX_CODE_SUCCESS:
return "Success";
case INFIX_CODE_UNKNOWN:
return "An unknown error occurred";
case INFIX_CODE_NULL_POINTER:
return "A required pointer argument was NULL";
infix/src/core/error.c view on Meta::CPAN
const char * msg = _get_error_message_for_code(code);
_INFIX_SAFE_STRNCPY(g_infix_last_error.message, msg, sizeof(g_infix_last_error.message) - 1);
}
}
/**
* @internal
* @brief Sets a detailed system error with a platform-specific error code and message.
*
* @details This is used for errors originating from OS-level functions like `dlopen`,
* `mmap`, or `VirtualAlloc`. It records both the `infix` error code and the
* underlying system error code (`errno` or `GetLastError`).
*
* @param category The category of the error.
* @param code The `infix` error code that corresponds to the failure.
* @param system_code The OS-specific error code (e.g., from `errno` or `GetLastError`).
* @param msg An optional custom message from the OS (e.g., from `dlerror`). If `nullptr`, the default message for
* `code` is used.
*/
void _infix_set_system_error(infix_error_category_t category,
infix_error_code_t code,
infix/src/core/signature.c view on Meta::CPAN
_print(state, "C");
_infix_type_print_itanium_recursive(state, type->meta.complex_info.base_type);
_add_itanium_sub(state, type);
break;
case INFIX_TYPE_VECTOR:
_print(state, "Dv%zu_", type->size / type->meta.vector_info.element_type->size);
_infix_type_print_itanium_recursive(state, type->meta.vector_info.element_type);
_add_itanium_sub(state, type);
break;
default:
// Fallback for types that don't map cleanly to Itanium mangling
_print(state, "v");
break;
}
}
/**
* @internal
* @brief Recursively prints the MSVC C++ mangling for a type.
* @param state The printer state.
* @param type The type to mangle.
*/
infix/src/core/types.c view on Meta::CPAN
#endif
case INFIX_PRIMITIVE_FLOAT16:
return &_infix_type_float16;
case INFIX_PRIMITIVE_FLOAT:
return &_infix_type_float;
case INFIX_PRIMITIVE_DOUBLE:
return &_infix_type_double;
case INFIX_PRIMITIVE_LONG_DOUBLE:
#if defined(INFIX_COMPILER_MSVC) || (defined(INFIX_OS_WINDOWS) && defined(INFIX_COMPILER_CLANG))
// On MSVC and macOS/Clang (sometimes), long double is an alias for double.
// We map to the double singleton to maintain type identity.
return &_infix_type_double;
#else
// On MinGW and Linux, long double is distinct (16 bytes).
// We MUST use the distinct type to handle layout and passing correctly.
return &_infix_type_long_double;
#endif
default:
// Return null for any invalid primitive ID.
return nullptr;
}
infix/src/core/types.c view on Meta::CPAN
if (!temp_arena)
return;
recalc_visited_node_t * visited_head = nullptr;
_infix_type_recalculate_layout_recursive(temp_arena, type, &visited_head);
infix_arena_destroy(temp_arena);
}
/**
* @internal
* @struct memo_node_t
* @brief A memoization node for the deep copy algorithm.
* @details This temporary structure maps a source `infix_type` address to its
* newly copied destination address. It is used to prevent re-copying the same
* object and to correctly reconstruct cyclic type graphs.
*/
typedef struct memo_node_t {
const infix_type * src; /**< The original type object's address. */
infix_type * dest; /**< The copied type object's address. */
struct memo_node_t * next; /**< The next node in the memoization list. */
} memo_node_t;
/**
* @internal
infix/src/jit/executor.c view on Meta::CPAN
* @file executor.c
* @brief Implements platform-specific memory management for JIT code and execution.
* @ingroup internal_jit
*
* @details This module serves as the critical OS abstraction layer for the JIT engine.
* Its primary responsibilities are:
*
* 1. **Executable Memory Management:** It allocates, protects, and frees executable
* memory in a way that is secure and compliant with modern OS security features
* like **W^X (Write XOR Execute)**. It implements different strategies (single-
* vs. dual-mapping) depending on the platform's capabilities and security model.
*
* 2. **Security Hardening:** It provides mechanisms to make memory regions read-only,
* which is used to protect the `infix_reverse_t` context from runtime memory
* corruption. It also implements "guard pages" on freed memory to immediately
* catch use-after-free bugs.
*
* 3. **Universal Dispatch:** It contains the `infix_internal_dispatch_callback_fn_impl`,
* the universal C entry point that is the final target of all reverse trampoline
* stubs. This function is the bridge between the low-level JIT code and the
* high-level user-provided C handlers.
infix/src/jit/executor.c view on Meta::CPAN
#include <fcntl.h>
#include <pthread.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <unistd.h>
#endif
#if defined(INFIX_OS_MACOS)
#include <dlfcn.h>
#include <libkern/OSCacheControl.h>
#endif
// Polyfills for mmap flags for maximum POSIX compatibility.
#if defined(INFIX_ENV_POSIX) && !defined(INFIX_OS_WINDOWS)
#if !defined(MAP_ANON) && defined(MAP_ANONYMOUS)
#define MAP_ANON MAP_ANONYMOUS
#endif
static pthread_mutex_t g_dwarf_mutex = PTHREAD_MUTEX_INITIALIZER;
#endif
#if defined(INFIX_OS_WINDOWS) && defined(INFIX_ARCH_X64)
// SEH Unwind Info Opcodes and Structures for JIT code on Windows x64.
// These are defined in winnt.h but we redefine them here for clarity and to ensure availability.
infix/src/jit/executor.c view on Meta::CPAN
// Hardened POSIX Anonymous Shared Memory Allocator (for Dual-Mapping W^X)
#if !defined(INFIX_OS_WINDOWS) && !defined(INFIX_OS_MACOS) && !defined(INFIX_OS_ANDROID) && !defined(INFIX_OS_OPENBSD)
#include <fcntl.h>
#include <stdint.h>
#if defined(INFIX_OS_LINUX) && defined(_GNU_SOURCE)
#include <sys/syscall.h>
#endif
/**
* @internal
* @brief Creates an anonymous file descriptor suitable for dual-mapping.
*
* @details Attempts multiple strategies in order of preference:
* 1. `memfd_create`: Modern Linux (kernel 3.17+). Best for security (no filesystem path).
* 2. `shm_open(SHM_ANON)`: FreeBSD/DragonFly. Automatic anonymity.
* 3. `shm_open(random_name)`: Fallback for older Linux/POSIX. Manually unlinked immediately.
*/
static int create_anonymous_file(void) {
#if defined(INFIX_OS_LINUX) && defined(MFD_CLOEXEC)
// Strategy 1: memfd_create (Linux 3.17+)
// MFD_CLOEXEC ensures the FD isn't leaked to child processes.
infix/src/jit/executor.c view on Meta::CPAN
#else
infix_executable_t exec = {.rx_ptr = nullptr, .rw_ptr = nullptr, .size = 0, .shm_fd = -1, .eh_frame_ptr = nullptr};
#endif
if (size == 0)
return exec;
#if defined(INFIX_OS_WINDOWS)
// Add headroom for SEH metadata on Windows.
size_t total_size = size + INFIX_SEH_METADATA_SIZE;
// Windows: Single-mapping W^X. Allocate as RW, later change to RX via VirtualProtect.
void * code = VirtualAlloc(nullptr, total_size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (code == nullptr) {
_infix_set_system_error(
INFIX_CATEGORY_ALLOCATION, INFIX_CODE_EXECUTABLE_MEMORY_FAILURE, GetLastError(), nullptr);
return exec;
}
exec.rw_ptr = code;
exec.rx_ptr = code;
#elif defined(INFIX_OS_MACOS) || defined(INFIX_OS_ANDROID) || defined(INFIX_OS_OPENBSD) || defined(INFIX_OS_DRAGONFLY)
// Single-mapping POSIX platforms. Allocate as RW, later change to RX via mprotect.
void * code = MAP_FAILED;
#if defined(MAP_ANON)
int flags = MAP_PRIVATE | MAP_ANON;
#if defined(INFIX_OS_MACOS)
// On macOS, we perform a one-time check for JIT support.
static bool g_use_secure_jit_path = false;
static bool g_checked_jit_support = false;
if (!g_checked_jit_support) {
g_use_secure_jit_path = has_jit_entitlement();
INFIX_DEBUG_PRINTF("macOS JIT check: Entitlement found = %s. Using %s API.",
g_use_secure_jit_path ? "yes" : "no",
g_use_secure_jit_path ? "secure (MAP_JIT)" : "legacy (mprotect)");
g_checked_jit_support = true;
}
// If entitled, use the modern, more secure MAP_JIT flag.
if (g_use_secure_jit_path)
flags |= MAP_JIT;
#endif // INFIX_OS_MACOS
code = mmap(nullptr, size, PROT_READ | PROT_WRITE, flags, -1, 0);
#if defined(INFIX_OS_MACOS)
if (code != MAP_FAILED && g_use_secure_jit_path) {
// Switch thread to Write mode. enabled=0 means Write allowed.
g_macos_apis.pthread_jit_write_protect_np(0);
}
#endif
#endif // MAP_ANON
if (code == MAP_FAILED) { // Fallback for older systems without MAP_ANON
int fd = open("/dev/zero", O_RDWR);
if (fd != -1) {
code = mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0);
close(fd);
}
}
if (code == MAP_FAILED) {
_infix_set_system_error(INFIX_CATEGORY_ALLOCATION, INFIX_CODE_EXECUTABLE_MEMORY_FAILURE, errno, nullptr);
return exec;
}
exec.rw_ptr = code;
exec.rx_ptr = code;
#else
// Dual-mapping POSIX platforms (e.g., Linux, FreeBSD). Create two separate views of the same memory.
exec.shm_fd = create_anonymous_file();
if (exec.shm_fd < 0) {
_infix_set_system_error(
INFIX_CATEGORY_ALLOCATION, INFIX_CODE_EXECUTABLE_MEMORY_FAILURE, errno, "create_anonymous_file failed");
return exec;
}
if (ftruncate(exec.shm_fd, size) != 0) {
_infix_set_system_error(
INFIX_CATEGORY_ALLOCATION, INFIX_CODE_EXECUTABLE_MEMORY_FAILURE, errno, "ftruncate failed");
close(exec.shm_fd);
exec.shm_fd = -1; // Ensure clean state
return exec;
}
// The RW mapping.
exec.rw_ptr = mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, exec.shm_fd, 0);
// The RX mapping of the exact same physical memory.
exec.rx_ptr = mmap(nullptr, size, PROT_READ | PROT_EXEC, MAP_SHARED, exec.shm_fd, 0);
// If either mapping fails, clean up both and return an error.
if (exec.rw_ptr == MAP_FAILED || exec.rx_ptr == MAP_FAILED) {
int err = errno; // Capture errno before cleanup
if (exec.rw_ptr != MAP_FAILED)
munmap(exec.rw_ptr, size);
if (exec.rx_ptr != MAP_FAILED)
munmap(exec.rx_ptr, size);
close(exec.shm_fd);
_infix_set_system_error(INFIX_CATEGORY_ALLOCATION, INFIX_CODE_EXECUTABLE_MEMORY_FAILURE, err, "mmap failed");
return (infix_executable_t){.rx_ptr = nullptr, .rw_ptr = nullptr, .size = 0, .shm_fd = -1};
}
// The mmap mappings hold a reference to the shared memory object, so we don't
// need the FD anymore. Keeping it open consumes a file descriptor per trampoline,
// causing "shm_open failed" after ~1024 trampolines.
close(exec.shm_fd);
exec.shm_fd = -1;
#endif
exec.size = size;
INFIX_DEBUG_PRINTF("Allocated JIT memory. RW at %p, RX at %p", exec.rw_ptr, exec.rx_ptr);
return exec;
}
infix/src/jit/executor.c view on Meta::CPAN
RtlDeleteFunctionTable((PRUNTIME_FUNCTION)exec.seh_registration);
#endif
if (exec.rw_ptr) {
// Change protection to NOACCESS to catch use-after-free bugs immediately.
if (!VirtualProtect(exec.rw_ptr, exec.size, PAGE_NOACCESS, &(DWORD){0}))
INFIX_DEBUG_PRINTF("WARNING: VirtualProtect failed to set PAGE_NOACCESS guard page.");
VirtualFree(exec.rw_ptr, 0, MEM_RELEASE);
}
#elif defined(INFIX_OS_MACOS)
// On macOS with MAP_JIT, the memory is managed with special thread-local permissions.
// We only need to unmap the single mapping.
if (exec.rw_ptr) {
// Creating a guard page before unmapping is good practice.
mprotect(exec.rw_ptr, exec.size, PROT_NONE);
munmap(exec.rw_ptr, exec.size);
}
#elif defined(INFIX_OS_ANDROID) || defined(INFIX_OS_OPENBSD) || defined(INFIX_OS_DRAGONFLY)
// Other single-mapping POSIX systems.
if (exec.rw_ptr) {
mprotect(exec.rw_ptr, exec.size, PROT_NONE);
munmap(exec.rw_ptr, exec.size);
}
#else
// Dual-mapping POSIX: protect and unmap both views.
if (exec.eh_frame_ptr) {
extern void __deregister_frame(void *);
pthread_mutex_lock(&g_dwarf_mutex);
__deregister_frame(exec.eh_frame_ptr);
pthread_mutex_unlock(&g_dwarf_mutex);
infix_free(exec.eh_frame_ptr);
}
if (exec.rx_ptr)
mprotect(exec.rx_ptr, exec.size, PROT_NONE);
if (exec.rw_ptr)
munmap(exec.rw_ptr, exec.size);
if (exec.rx_ptr && exec.rx_ptr != exec.rw_ptr) // rw_ptr might be same as rx_ptr on some platforms
munmap(exec.rx_ptr, exec.size);
if (exec.shm_fd >= 0)
close(exec.shm_fd);
#endif
}
/**
* @internal
* @brief Makes a block of JIT memory executable and flushes instruction caches.
*
* @details This function completes the W^X process.
* - On single-mapping platforms, it changes the memory protection from RW to RX.
* - On dual-mapping platforms, this is a no-op as the RX mapping already exists.
*
* Crucially, it also handles flushing the CPU's instruction cache on architectures
* that require it (like AArch64). This is necessary because the CPU may have cached
* old (zeroed) data from the memory location, and it must be explicitly told to
* re-read the newly written machine code instructions.
*
* @param exec The executable memory block.
* @param category The category of the trampoline.
* @param prologue_size The size of the prologue.
* @return `true` on success, `false` on failure.
infix/src/jit/executor.c view on Meta::CPAN
// Switch thread state to Execute allowed (enabled=1)
g_macos_apis.pthread_jit_write_protect_np(1);
result = true;
}
else {
result = (mprotect(exec->rw_ptr, exec->size, PROT_READ | PROT_EXEC) == 0);
}
if (!result)
_infix_set_system_error(INFIX_CATEGORY_ALLOCATION, INFIX_CODE_PROTECTION_FAILURE, errno, nullptr);
#elif defined(INFIX_OS_ANDROID) || defined(INFIX_OS_OPENBSD) || defined(INFIX_OS_DRAGONFLY)
// Other single-mapping POSIX platforms use mprotect.
result = (mprotect(exec->rw_ptr, exec->size, PROT_READ | PROT_EXEC) == 0);
if (!result)
_infix_set_system_error(INFIX_CATEGORY_ALLOCATION, INFIX_CODE_PROTECTION_FAILURE, errno, nullptr);
#else
// Dual-mapping POSIX (Linux, FreeBSD).
// The RX mapping is already executable.
#if defined(INFIX_OS_LINUX) && defined(INFIX_ARCH_X64)
_infix_register_eh_frame_linux_x64(exec, category);
#elif defined(INFIX_OS_LINUX) && defined(INFIX_ARCH_AARCH64)
_infix_register_eh_frame_arm64(exec, category);
#endif
// SECURITY CRITICAL: We MUST unmap the RW view now. If we leave it mapped,
// an attacker with a heap disclosure could find it and overwrite the JIT code,
// bypassing W^X.
if (munmap(exec->rw_ptr, exec->size) == 0) {
exec->rw_ptr = nullptr; // Clear the pointer to prevent double-free or misuse.
result = true;
}
else {
_infix_set_system_error(
INFIX_CATEGORY_ALLOCATION, INFIX_CODE_PROTECTION_FAILURE, errno, "munmap of RW view failed");
result = false;
}
#endif
if (result)
INFIX_DEBUG_PRINTF("Memory at %p is now executable.", exec->rx_ptr);
return result;
}
// Public API: Protected (Read-Only) Memory
/**
* @internal
infix/src/jit/executor.c view on Meta::CPAN
c23_nodiscard infix_protected_t infix_protected_alloc(size_t size) {
infix_protected_t prot = {.rw_ptr = nullptr, .size = 0};
if (size == 0)
return prot;
#if defined(INFIX_OS_WINDOWS)
prot.rw_ptr = VirtualAlloc(nullptr, size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (!prot.rw_ptr)
_infix_set_system_error(INFIX_CATEGORY_ALLOCATION, INFIX_CODE_OUT_OF_MEMORY, GetLastError(), nullptr);
#else
#if defined(MAP_ANON)
prot.rw_ptr = mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0);
#else
int fd = open("/dev/zero", O_RDWR);
if (fd == -1)
prot.rw_ptr = MAP_FAILED;
else {
prot.rw_ptr = mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0);
close(fd);
}
#endif
if (prot.rw_ptr == MAP_FAILED) {
_infix_set_system_error(INFIX_CATEGORY_ALLOCATION, INFIX_CODE_OUT_OF_MEMORY, errno, nullptr);
prot.rw_ptr = nullptr;
}
#endif
if (prot.rw_ptr)
prot.size = size;
infix/src/jit/executor.c view on Meta::CPAN
* @internal
* @brief Frees a block of protected memory.
* @param prot The memory block to free.
*/
void infix_protected_free(infix_protected_t prot) {
if (prot.size == 0)
return;
#if defined(INFIX_OS_WINDOWS)
VirtualFree(prot.rw_ptr, 0, MEM_RELEASE);
#else
munmap(prot.rw_ptr, prot.size);
#endif
}
/**
* @internal
* @brief Makes a block of memory read-only for security hardening.
*
* @details This function is called on the `infix_reverse_t` context after it has been
* fully initialized. By making the context read-only, it helps prevent bugs or
* security vulnerabilities from corrupting critical state like function pointers.
*
lib/Affix.c view on Meta::CPAN
entry->ref_count--;
if (entry->ref_count == 0) {
STRLEN klen;
const char * kstr = HePV(he, klen);
SV * key_sv = newSVpvn(kstr, klen);
if (HeKUTF8(he))
SvUTF8_on(key_sv);
// On Linux, dlclose() is notoriously dangerous for libraries that
// spawn background threads or register global handlers (Go, .NET, Audio, etc.)
// unmapping the code while these threads are active causes a SEGV.
#if defined(__linux__) || defined(__linux)
// Leak the library handle but free our wrapper
infix_free(entry->lib);
#else
infix_library_close(entry->lib);
#endif
safefree(entry);
hv_delete_ent(MY_CXT.lib_registry, key_sv, G_DISCARD, 0);
SvREFCNT_dec(key_sv);
}
lib/Affix.c view on Meta::CPAN
sv_setiv(sv, val);
// Look up the Name
dMY_CXT;
const char * type_name = infix_type_get_name(type);
if (type_name) {
SV ** enum_info_ptr = hv_fetch(MY_CXT.enum_registry, type_name, strlen(type_name), 0);
if (enum_info_ptr) {
HV * enum_info = (HV *)SvRV(*enum_info_ptr);
SV ** enum_map_ptr = hv_fetch(enum_info, "vals", 4, 0);
if (enum_map_ptr) {
HV * enum_map = (HV *)SvRV(*enum_map_ptr);
// Look up the integer value in the hash
// Keys in Perl hashes are strings, so we format the IV.
char key[64];
snprintf(key, 64, "%" IVdf, val);
SV ** name_sv = hv_fetch(enum_map, key, strlen(key), 0);
if (name_sv && SvPOK(*name_sv)) {
// Set the String Value (creating Dualvar)
// sv_setpv overwrites the IV. We need to set PV while keeping IOK.
const char * name_str = SvPV_nolen(*name_sv);
sv_setpv(sv, name_str); // Sets PV, clears IV? No, usually clears flags, right?
// Force dualvar state by manually reinstating the IV
SvIV_set(sv, val);
SvIOK_on(sv); // It is valid Integer
// SvPOK is on from sv_setpv
lib/Affix.c view on Meta::CPAN
push_reverse_trampoline(aTHX_ affix, type, perl_sv, c_ptr);
break;
case INFIX_TYPE_ENUM:
if (SvPOK(perl_sv)) {
dMY_CXT;
const char * type_name = infix_type_get_name(type);
if (type_name) {
SV ** enum_info_ptr = hv_fetch(MY_CXT.enum_registry, type_name, strlen(type_name), 0);
if (enum_info_ptr) {
HV * enum_info = (HV *)SvRV(*enum_info_ptr);
SV ** enum_map_ptr = hv_fetch(enum_info, "consts", 6, 0);
if (enum_map_ptr) {
HV * enum_map = (HV *)SvRV(*enum_map_ptr);
STRLEN len;
const char * str = SvPV(perl_sv, len);
SV ** val_sv = hv_fetch(enum_map, str, len, 0);
if (val_sv) {
sv2ptr(aTHX_ affix, *val_sv, c_ptr, type->meta.enum_info.underlying_type);
return;
}
}
}
}
}
sv2ptr(aTHX_ affix, perl_sv, c_ptr, type->meta.enum_info.underlying_type);
break;
lib/Affix.c view on Meta::CPAN
if (entry->ref_count > 0)
warn("Affix: library handle for '%s' has %d outstanding references at END.",
HeKEY(he),
(int)entry->ref_count);
#endif
// Temp fix: Disable library unloading at process exit.
//
// Many modern C libraries (WebUI, Go runtimes, Audio libs) spawn background
// threads that persist until the process dies. If we dlclose() the library
// here, the code segment is unmapped. When the background thread wakes up
// to do cleanup or work, it executes garbage memory and segfaults.
//
// Since the process is ending, the OS will reclaim file handles and memory
// automatically. It's (in my opinion) safer to leak the handle than to crash the process.
#if defined(__linux__) || defined(__linux)
// Leak the library handle but free our wrapper
if (entry->lib)
infix_free(entry->lib);
#else
// This extra symbol check is here to prevent shared libs written in Go from crashing Affix.
lib/Affix.c view on Meta::CPAN
if (elem_size == 0)
croak("Cannot index into zero-sized type");
void * target = (char *)pin->pointer + (index * elem_size);
sv2ptr(aTHX_ nullptr, val_sv, target, elem_type);
XSRETURN_EMPTY;
}
// Helper to register core internal types
static void _register_core_types(infix_registry_t * registry) {
// Register SV as a named type (dummy struct ensures it keeps the name in the registry).
// This allows signature parsing of "@SV" or "SV" (via hack) to map to a named opaque type.
// Direct usage of this type is blocked in get_opcode_for_type; it must be wrapped in Pointer[].
if (infix_register_types(registry, "@SV = { __sv_opaque: uint8 };") != INFIX_SUCCESS)
croak("Failed to register internal type alias '@SV'");
// We register File and PerlIO as opaque structs.
// This semantically matches C's FILE struct which (for now) will remain opaque to the user.
// We require "Pointer[File]" to mean "FILE*"
if (infix_register_types(registry, "@File = { _opaque: [0:uchar] };") != INFIX_SUCCESS)
croak("Failed to register internal type alias '@File'");
if (infix_register_types(registry, "@PerlIO = { _opaque: [0:uchar] };") != INFIX_SUCCESS)
lib/Affix.pm view on Meta::CPAN
#~ warn "Sun: $is_sun";
CORE::state $libdirs;
if ( !defined $libdirs ) {
if ($is_win) {
require Win32;
$libdirs = [ Win32::GetFolderPath( Win32::CSIDL_SYSTEM() ) . '/', Win32::GetFolderPath( Win32::CSIDL_WINDOWS() ) . '/', ];
}
else {
$libdirs = [
( split ' ', $Config{libsdirs} ),
map { split /[:;]/, ( $ENV{$_} ) } grep { $ENV{$_} } qw[LD_LIBRARY_PATH DYLD_LIBRARY_PATH DYLD_FALLBACK_LIBRARY_PATH]
];
}
no warnings qw[once];
require DynaLoader;
$libdirs = [
grep { -d $_ } map { rel2abs($_) } qw[. ./lib ~/lib /usr/local/lib /usr/lib /lib /usr/lib/system], @DynaLoader::dl_library_path,
@$libdirs
];
}
CORE::state $regex;
if ( !defined $regex ) {
$regex = $is_win ?
qr/^
(?:lib)?(?<name>\w+)
(?:[_-](?<version>[0-9\-\._]+))?_*
\.$Config{so}
lib/Affix.pm view on Meta::CPAN
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
sub signature { my $self = shift; $self->{type}->signature . ':' . $self->{width} }
}
package #
Affix::Type::Enum {
our @ISA = qw[Affix::Type];
use Carp;
sub signature { 'e:' . shift->{type} }
sub resolve {
my $self = shift;
return ( $self->{const_map}, $self->{values_map} ) if defined $self->{values_map};
$self->{const_map} = {};
$self->{values_map} = {};
my $counter = 0;
for my $item ( @{ $self->{elements} } ) {
my ( $name, $final_val );
if ( !ref $item ) {
$name = $item;
$final_val = $counter;
}
elsif ( ref $item eq 'ARRAY' ) {
my $raw_val;
( $name, $raw_val ) = @$item;
if ( $raw_val =~ /^-?\d+$/ ) {
$final_val = $raw_val;
}
elsif ( $raw_val =~ /^0x[0-9a-fA-F]+$/ ) {
$final_val = hex($raw_val);
}
else {
$final_val = $self->_calculate_expr( $raw_val, $self->{const_map} );
}
}
else {
Carp::croak("Enum elements must be Strings or [Name => Value] ArrayRefs");
}
$self->{const_map}->{$name} = $final_val;
$self->{values_map}->{$final_val} //= $name;
$counter = $final_val + 1;
}
return ( $self->{const_map}, $self->{values_map} );
}
sub _calculate_expr {
my ( $self, $expr, $lookup ) = @_;
use integer;
my @tokens = $expr =~ /(0x[0-9a-fA-F]+|\d+|[a-zA-Z_]\w*|<<|>>|&&|\|\||==|!=|<=|>=|[+\-*\/%|&^~!?:()<>])/g;
for my $t (@tokens) {
next if $t =~ /^(?:<<|>>|&&|\|\||==|!=|<=|>=|[+\-*\/%|&^~!?:()<>])$/;
next if $t =~ /^\d+$/;
next if $t =~ /^0x/;
lib/Affix.pm view on Meta::CPAN
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;
lib/Affix.pod view on Meta::CPAN
pin $errno, libc(), 'errno', Int;
$errno = 0; # Writes directly to C memory
=head2 C<unpin( $var )>
Removes the magic applied by C<pin>. The variable retains its last value but is no longer linked to C memory.
=head1 TYPE SYSTEM
Affix signatures are built using helper functions that map precisely to C types. These are exported by default, or can
be imported explicitly using the C<:types> tag.
=head2 Primitive Types
=head3 Void & Booleans
=over
=item * C<Void>: Used for functions that return nothing (C<void>).
lib/Affix.pod view on Meta::CPAN
=item * C<UChar>: Unsigned C<unsigned char>.
=item * C<WChar>: Wide character (C<wchar_t>), usually 16-bit on Windows and 32-bit on Linux/macOS.
=item * C<Char8>, C<Char16>, C<Char32>: Explicit-width C++ character types (C<char8_t>, etc.).
=back
=head3 Platform-Native Integers
These types map to the system's native bit-widths (e.g., C<Long> is 32-bit on Windows x64, but 64-bit on Linux x64).
=over
=item * C<Short> / C<UShort>: C<short> / C<unsigned short>.
=item * C<Int> / C<UInt>: C<int> / C<unsigned int> (typically 32-bit).
=item * C<Long> / C<ULong>: C<long> / C<unsigned long>.
=item * C<LongLong> / C<ULongLong>: C<long long> / C<unsigned long long> (guaranteed at least 64-bit).
lib/Affix.pod view on Meta::CPAN
=item * C<Double> / C<Float64>: Standard 64-bit C<double>.
=item * C<LongDouble>: Platform-specific extended precision (typically 80-bit on x86 or 128-bit).
=back
=head3 Complex Numbers
=over
=item * C<Complex[ $type ]>: C99 complex numbers (e.g., C<Complex[Double]>). In Perl, these map to an C<ArrayRef> of two numbers: C<[ $real, $imaginary ]>.
=back
=head2 String Types
=over
=item * B<C<String>>: Maps to C<const char*>. Affix handles UTF-8 encoding (Perl to C) and decoding (C to Perl) automatically.
=item * B<C<WString>>: Maps to C<const wchar_t*>. Affix automatically handles UTF-16/UTF-32 conversions, including Windows Surrogate Pairs.
lib/Affix.pod view on Meta::CPAN
=item * B<C<SockAddr>>: Specialized marshalling for packed socket strings (e.g., from C<Socket::pack_sockaddr_in>) to C<struct sockaddr*>.
=item * B<C<SV>>: Direct, low-level access to Perl's internal Interpreter Object (C<SV*>). B<Must> be wrapped in a pointer: C<Pointer[SV]>.
=back
=head2 Aggregate Types
=head3 C<Struct[ @members ]>
A C struct, mapped to a Perl C<HashRef>.
# C: typedef struct { int x; int y; } Point;
typedef Point => Struct[ x => Int, y => Int ];
=head3 C<Union[ @members ]>
A C union, mapped to a Perl C<HashRef> with exactly one key.
# C: union { int key_code; float pressure; };
typedef Event => Union[ key_code => Int, pressure => Float ];
=head3 C<Packed[ $align, $aggregate ]>
Forces specific byte alignment on a Struct or Union (e.g., C<#pragma pack(1)>).
# C: #pragma pack(push, 1) ...
Packed[ 1, Struct[ flag => Char, data => Int ] ];
lib/Affix.pod view on Meta::CPAN
=item * B<Linux/macOS (System V AMD64 ABI):> Arguments are passed in C<rdi, rsi, rdx, rcx, r8, r9>, with the rest on the stack.
=item * B<Windows (Microsoft x64):> Arguments are passed in C<rcx, rdx, r8, r9>, with "shadow space" reserved on the stack.
=back
=head2 Go
Go libraries can be loaded if they are compiled with C<-buildmode=c-shared>. Note that Go slices and strings contain
internal metadata (length/capacity) and do not map directly to C arrays or C<char*>. Use the C<C> package inside Go
(C<import "C">) and C<*C.char> to bridge the boundary.
=head1 ERROR HANDLING & DEBUGGING
Bridging two entirely different runtimes can lead to spectacular crashes if types or memory boundaries are mismatched.
Affix provides built-in tools to help you identify what went wrong.
=head2 Error Handling
=head3 C<errno()>
lib/Affix/Build.pm view on Meta::CPAN
# 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...
@cflags = map { chomp; $_ } grep { defined && length } Text::ParseWords::parse_line( q/ /, 1, $flags->{cflags} // '' );
@cxxflags = map { chomp; $_ } grep { defined && length } Text::ParseWords::parse_line( q/ /, 1, $flags->{cxxflags} // '' );
@ldflags = map { chomp; $_ } grep { defined && length } Text::ParseWords::parse_line( q/ /, 1, $flags->{ldflags} // '' );
}
method add ( $input, %args ) {
$_lib = (); # Reset cached library handle
my ( $path, $lang );
if ( ref $input eq 'SCALAR' ) { # Inline source code
$args{lang} // croak q[Parameter 'lang' (extension) is required for inline source];
$lang = lc $args{lang};
# Generate a unique filename in the build dir
lib/Affix/Build.pm view on Meta::CPAN
# Handle local flags
my $local_flags = $args{flags} // [];
$local_flags = [ split ' ', $local_flags ] unless builtin::reftype $local_flags eq 'ARRAY';
push @sources, { path => $path, lang => $lang, flags => $local_flags };
}
method compile_and_link () {
croak "No sources added" unless @sources;
# Check if we are mixing languages
my %langs = map { $_->{lang} => 1 } @sources;
if ( ( scalar keys %langs ) > 1 ) {
return $self->_strategy_polyglot();
}
return $_lib = $self->_strategy_native();
}
method link { $_lib //= $self->compile_and_link(); $_lib }
# Used when only one language is present. We delegate the entire build process
# to that language's compiler to produce the final shared library.
method _strategy_native () {
lib/Affix/Build.pm view on Meta::CPAN
my %seen;
for my $l (@libs) {
next if $seen{$l}++;
push @cmd, "-l$l";
}
push @cmd, @ldflags;
$self->_run(@cmd);
return $libname;
}
# Helper to map extensions to method names
method _resolve_handler ($l) {
# Normalize
if ( $l =~ /^(cpp|cxx|cc|c\+\+)$/ ) { return '_build_cpp'; }
elsif ( $l =~ /^(f|f90|f95|for|fortran)$/ ) { return '_build_fortran'; }
elsif ( $l =~ /^(ru?st?)$/ ) { return '_build_rust'; }
elsif ( $l =~ /^(cs|csharp|c#)$/ ) { return '_build_csharp'; }
elsif ( $l =~ /^(fs|fsharp|f#)$/ ) { return '_build_fsharp'; }
elsif ( $l =~ /^(s|asm|assembly)$/ ) { return '_build_asm'; }
elsif ( $l =~ /^(adb|ads|ada)$/ ) { return '_build_ada'; }
lib/Affix/Platform/BSD.pm view on Meta::CPAN
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*(.+)$];
( $cache->{$name}{$version} ) = map { -l $_ ? readlink($_) : $_ } map { $_ =~ $regex; defined $1 ? $1 : () } split /\n\s*/,
`export LC_ALL 'C'; export LANG 'C'; /sbin/ldconfig -r`;
$cache->{$name}{$version} // ();
}
};
1;
lib/Affix/Platform/Unix.pm view on Meta::CPAN
'PPC64-64' => 'libc6,64bit',
'SPARC64-64' => 'libc6,64bit',
'Itanium-64' => 'libc6,IA-64',
'ARM64-64' => 'libc6,AArch64'
}->{$lookup_key};
# If this specific architecture/bitness combination isn't in our list, return nothing.
$machine // return;
# XXX assuming GLIBC's ldconfig (with option -p)
grep { is_elf($_) } map {
/^(?:lib)?${name}(?:\-\S+)?\.\s*.*\(${machine}.*\)\s+=>\s+(.+)$/;
defined $1 ? path($1)->realpath : ()
} split /\R\s*/, `export LC_ALL 'C'; export LANG 'C'; /sbin/ldconfig -p 2>&1`;
}
sub _findLib_dynaloader($name) {
DynaLoader::dl_findfile( '-l' . $name );
}
sub _findLib_ld($name) {
lib/Affix/Platform/Unix.pm view on Meta::CPAN
$name = readlink $name if -l $name; # Handle symbolic links
return $name if is_elf($name);
}
CORE::state $cache;
unless ( defined $cache->{$name}{$version} ) {
my @ret = grep { is_elf($_) } _findLib_dynaloader($name);
@ret = grep { is_elf($_) } _findLib_ldconfig($name) unless @ret;
@ret = grep { is_elf($_) } _findLib_gcc($name) unless @ret;
@ret = grep { is_elf($_) } _findLib_ld($name) unless @ret;
return unless @ret;
for my $lib ( map { path($_)->realpath } @ret ) {
next unless $lib =~ /^.*?\/lib$name.*\.$so(?:\.([\d\.\-]+))?$/;
$version = $1 if defined $1 && $version eq '';
$cache->{$name}{$version} //= $lib;
}
}
$cache->{$name}{$version} // ();
}
sub _get_soname ($file) { # assuming GNU binutils / ELF
return undef unless $file && -f $file;
lib/Affix/Platform/Windows.pm view on Meta::CPAN
elsif ( $version <= 13 ) {
$clibname = sprintf( 'msvcr%d', $version * 10 );
}
else {
# CRT not directly loadable (see python/cpython#23606)
return undef;
}
# Check for debug build
my $debug_suffix = '_d'; # Assuming debug suffix is '_d'
my $suffixes = join '|', map quotemeta, @DynaLoader::dl_extensions;
if ( $debug_suffix =~ /$suffixes/ ) {
$clibname .= $debug_suffix;
}
return "$clibname.dll";
}
sub get_msvcrt_version() {
open( my $pipe, '-|', 'dumpbin /headers msvcrt.dll', 'r' ) or return;
my $dumpbin_output;
$dumpbin_output .= $_ while <$pipe>;
lib/Affix/Wrap.pm view on Meta::CPAN
$t =~ s/__attribute__\s*\(\(.*\)\)//g;
$t =~ s/^\s+|\s+$//g;
# Function Pointer: Ret (*)(Args)
if ( $t =~ /^(.+?)\s*\(\*\)\s*\((.*)\)$/ ) {
my $ret_str = $1;
my $args_str = $2;
my $ret = $class->parse($ret_str);
my @args;
if ( $args_str ne '' && $args_str ne 'void' ) {
@args = map { $class->parse($_) } split( /\s*,\s*/, $args_str );
}
return Affix::Wrap::Type::CodeRef->new( ret => $ret, params => \@args );
}
if ( $t =~ /^(.*)\s*\[(\d+)\]$/ ) {
return Affix::Wrap::Type::Array->new( of => $class->parse($1), count => $2 );
}
$t =~ s/(\*)\s*(?:const|restrict)\s*$/$1/;
$t =~ /^(.+)\s*\*$/ ? Affix::Wrap::Type::Pointer->new( of => $class->parse($1) ) : $class->new( name => $t );
}
method affix_type {
my $t = $self->name;
$t =~ s/^(?:struct|union|enum)\s+//;
$t =~ s/consts?\s+//g;
$t =~ s/(\s+\**)const$/$1/g;
$t =~ s/(\s+\**)restrict$/$1/g;
$t =~ s/\s+$//;
#
state $type_map //= {
void => 'Void',
bool => 'Bool',
short => 'Short',
'unsigned short' => 'UShort',
char => 'Char',
'signed char' => 'SChar',
'unsigned char' => 'UChar',
int => 'Int',
'unsigned int' => 'UInt',
long => 'Long',
lib/Affix/Wrap.pm view on Meta::CPAN
'jmp_buf' => 'Void', # Opaque handle for setjmp
'_jbtype' => 'Void', # Internal jmp_buf typedef
# 'tm' (struct tm) is undefined because we skip system headers.
# Mapping to Void ensures 'struct tm *' becomes 'Pointer[Void]'
'tm' => Affix::Pointer( [Void] ),
'struct tm' => Affix::Pointer( [Void] )
};
# Case-insensitive lookup (handled by existing code)
return $type_map->{ lc $t } if defined $type_map->{ lc $t };
return "'$t'" if $t =~ /^[a-z_]\w*$/i;
warn "WARNING: Unknown C type '$t' mapped to Void\n";
'Void';
}
method affix {
use Affix qw[Void];
my $type_str = $self->affix_type;
# Case 1: Simple named type (e.g. "Int", "UChar")
if ( $type_str =~ /^(\w+)$/ ) {
no strict 'refs';
lib/Affix/Wrap.pm view on Meta::CPAN
method affix_type { sprintf( 'Array[%s, %d]', $of->affix_type, $count ) }
method affix { Array [ $of->affix, $count ] }
};
class #
Affix::Wrap::Type::CodeRef : isa(Affix::Wrap::Type) {
use Affix qw[Callback];
field $ret : reader : param;
field $params : reader : param; # ArrayRef of Types
method name {
sprintf '%s (*)(%s)', $ret->name, join( ', ', map { $_->name } @$params );
}
method affix_type {
sprintf 'Callback[[%s] => %s]', join( ', ', map { $_->affix_type } @$params ), $ret->affix_type;
}
method affix {
Callback [ [ map { $_->affix } @$params ], $ret->affix ];
}
};
class #
Affix::Wrap::Argument {
field $type : reader : param;
field $name : reader : param //= '';
method to_string { length($name) ? $type->to_string . ' ' . $name : $type->to_string }
use overload '""' => 'to_string', fallback => 1;
method affix_type { $type->affix_type }
method affix { $type->affix }
lib/Affix/Wrap.pm view on Meta::CPAN
$out .= $self->_format_pod( $d->{brief} ) . "\n\n" if length $d->{brief};
$out .= $self->_format_pod( $d->{desc} ) . "\n\n" if length $d->{desc};
# Format parameters
if ( keys %{ $d->{params} } ) {
$out .= "=over\n\n";
my @param_names = sort keys %{ $d->{params} };
# If we have args metadata (e.g. Function), use it for ordering
if ( $self->can('args') && ref( $self->args ) eq 'ARRAY' ) {
@param_names = map { $_->name } grep { exists $d->{params}{ $_->name } } @{ $self->args };
# Fallback for params documented but not in signature (rare but possible in C macros/varargs)
my %seen = map { $_ => 1 } @param_names;
push @param_names, grep { !$seen{$_} } sort keys %{ $d->{params} };
}
for my $name (@param_names) {
$out .= "=item C<$name>\n\n" . $self->_format_pod( $d->{params}{$name} ) . "\n\n";
}
$out .= "=back\n\n";
}
# Format return value
if ( length $d->{return} ) {
lib/Affix/Wrap.pm view on Meta::CPAN
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 //= [];
method affix_type {
my $type_name = $tag eq 'union' ? 'Union' : 'Struct';
sprintf '%s[ %s ]', $type_name, join( ', ', map { $_->name . ' => ' . $_->affix_type } @$members );
}
method affix ( $lib //= (), $pkg //= () ) {
use Affix qw[Struct Union];
if ( $tag eq 'union' ) {
return Union [ map { $_->name, $_->affix } @$members ];
}
Struct [ map { $_->name, $_->affix } @$members ];
}
} class #
Affix::Wrap::Enum : isa(Affix::Wrap::Entity) {
field $constants : reader : param //= [];
method affix_type {
my @defs;
for my $c (@$constants) {
if ( !defined $c->{value} ) {
push @defs, $c->{name};
lib/Affix/Wrap.pm view on Meta::CPAN
use Affix qw[Enum];
my @defs;
for my $c (@$constants) {
if ( !defined $c->{value} ) { push @defs, $c->{name}; next }
push @defs, [ $c->{name}, $c->{value} ];
}
my $type = Enum [@defs];
# Manual export if this is a bare enum (not typedef'd)
if ($pkg) {
my ( $const_map, $val_map ) = $type->resolve();
no strict 'refs';
while ( my ( $const_name, $val ) = each %$const_map ) {
*{"${pkg}::${const_name}"} = sub () {$val};
}
}
return $type;
}
};
class #
Affix::Wrap::Function : isa(Affix::Wrap::Entity) {
use Carp qw[];
use Affix qw[CodeRef];
lib/Affix/Wrap.pm view on Meta::CPAN
field $mangled_name : reader : param //= ();
method affix_type {
sprintf 'affix $lib, %s => [%s], %s',
( $self->mangled_name ne $self->name ? ( sprintf q[[%s => '%s']], $self->mangled_name, $self->name ) :
( sprintf q['%s'], $self->name ) ), join( ', ', @{ $self->affix_args } ), $self->affix_ret;
}
method affix ( $lib, $pkg //= () ) {
if ($lib) {
my $arg_types = [ map { $_->affix } @$args ];
my $_lib = Affix::load_library($lib);
my $ret_type = $ret->affix;
if ($pkg) {
no strict 'refs';
no warnings 'redefine';
Affix::affix(
$lib,
[ (
defined $self->mangled_name &&
$self->mangled_name ne $self->name &&
lib/Affix/Wrap.pm view on Meta::CPAN
Affix::find_symbol( $lib, $self->mangled_name ) ? [ $self->mangled_name, $self->name ] : $self->name,
$arg_types,
$ret_type
) // Carp::carp Affix::errno();
}
}
}
method affix_ret { $ret->affix_type }
method affix_args {
[ map { $_->affix_type } @$args ]
}
method call_ret { $ret->affix }
method call_args {
[ map { $_->affix } @$args ]
}
} class #
Affix::Wrap::Driver::Clang {
use Config;
field $project_files : param : reader;
field $allowed_files = {};
field $project_dirs = [];
field $paths_seen = {};
field $file_cache = {};
field $last_seen_file = undef;
lib/Affix/Wrap.pm view on Meta::CPAN
$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 = (
$clang, '-target', $self->_get_triple(), '-Xclang',
'-ast-dump=json', '-Xclang', '-detailed-preprocessing-record', '-fsyntax-only',
'-fparse-all-comments', '-Wno-everything', @includes, $ep_abs
);
my ( $stdout, $stderr, $exit ) = Capture::Tiny::capture { system(@cmd); };
if ( $exit != 0 ) { die "Clang Error:\n$stderr"; }
if ( $stdout =~ /^.*?(\{.*)/s ) { $stdout = $1; }
my $ast = JSON::PP::decode_json($stdout);
lib/Affix/Wrap.pm view on Meta::CPAN
$v =~ s/\/\/.*$//;
$v =~ s/\/\*.*?\*\///g;
$v =~ s/^\s+|\s+$//g;
return $v;
}
}
'';
}
method _scan_macros_fallback($acc) {
my %seen = map { $_->name => 1 } grep { ref($_) eq 'Affix::Wrap::Macro' } @$acc;
for my $f ( keys %$allowed_files ) {
next unless $self->_is_valid_file($f);
my $c = $self->_get_content($f);
while ( $c =~ /^\s*#\s*define\s+(\w+)(?:[ \t]+(.*?))?\s*$/mg ) {
my $name = $1;
next if $seen{$name};
my $val = $2 // '';
my $off = $-[0];
my $end = $+[0];
my $pre = substr( $c, 0, $off );
lib/Affix/Wrap.pm view on Meta::CPAN
my $content = $1;
my $s = $-[0];
my $e = $+[0];
$content =~ s/\s+/ /g;
$content =~ s/^\s+|\s+$//g;
if ( $content =~ /^(.+?)\s*\(\*\s*(\w+)\)\s*\((.*?)\)$/ ) {
my ( $ret_str, $name, $args_str ) = ( $1, $2, $3 );
my $ret = Affix::Wrap::Type->parse($ret_str);
my @args;
if ( defined $args_str && length $args_str && $args_str ne 'void' ) {
my @args_raw = grep {length} map { s/^\s+|\s+$//g; $_ } split /,(?![^(]*\))/, $args_str;
if ( @args_raw == 1 && $args_raw[0] =~ /^void$/ ) { @args_raw = (); }
for my $raw (@args_raw) {
if ( $raw =~ /^(.+?)([\s\*]+)([a-zA-Z_]\w*(?:\[.*?\])?)$/ ) {
my ( $t, $sep, $n ) = ( $1, $2, $3 );
$t .= $sep;
if ( $n =~ s/(\[.*\])$// ) { $t .= $1 }
push @args, Affix::Wrap::Type->parse($t);
}
else {
push @args, Affix::Wrap::Type->parse($raw);
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;
}
my $code_ref = Affix::Wrap::Type::CodeRef->new( ret => $ret, params => \@p );
push @args, Affix::Wrap::Argument->new( type => $code_ref, name => $cb_name );
}
elsif ( $raw =~ /^(.+?)([\s\*]+)([a-zA-Z_]\w*(?:\[.*?\])?)$/ ) {
my ( $t, $sep, $n ) = ( $1, $2, $3 );
$t .= $sep;
if ( $n =~ s/(\[.*\])$// ) { $t .= $1 }
push @args, Affix::Wrap::Argument->new( type => Affix::Wrap::Type->parse($t), name => $n );
}
lib/Affix/Wrap.pm view on Meta::CPAN
$pending_doc = '';
next;
}
# Function pointer: ret (*name)(args)
if ( $b =~ s/^\s*([\w\s\*]+?)\s*\(\*\s*(\w+)\)\s*\((.*?)\)\s*;// ) {
my ( $ret_str, $name, $args_str ) = ( $1, $2, $3 );
my $ret = Affix::Wrap::Type->parse($ret_str);
my @args;
if ( $args_str ne '' && $args_str ne 'void' ) {
@args = map { Affix::Wrap::Type->parse($_) } split( /\s*,\s*/, $args_str );
}
my $type_obj = Affix::Wrap::Type::CodeRef->new( ret => $ret, params => \@args );
push @m, Affix::Wrap::Member->new( name => $name, type => $type_obj, doc => $clean->($pending_doc) );
$pending_doc = '';
next;
}
if ( $b =~ s/^\s*(.+?)([\s\*]+)([a-zA-Z_]\w*(?:\[.*?\])?)\s*;// ) {
my ( $t, $sep, $n ) = ( $1, $2, $3 );
$t .= $sep;
$t =~ s/^\s+|\s+$//g;
lib/Affix/Wrap.pod view on Meta::CPAN
=head1 Supported Features
C<Affix::Wrap> extracts and bridges:
=over
=item * Function signatures (including pointer-to-function arguments)
=item * Nested Structs and Unions
=item * Enums (maps them to Affix Dualvar Enums)
=item * Macros (numeric and string constants)
=item * Typedefs (follows deep typedef chains)
=item * Extern Global Variables (binds them via C<Affix::pin>)
=item * Doxygen/Markdown Comments (extracts to POD when generating modules)
=back
lib/Affix/Wrap.pod view on Meta::CPAN
Required. An array reference of paths to the C header files (C<.h>, C<.hpp>, C<.hxx>) you wish to parse.
=item C<include_dirs>
Optional. An array reference of paths to search for C<#include "..."> directives. The directory of every file listed in
C<project_files> is automatically added to this list.
=item C<types>
Optional. A hash reference for manually mapping type names to L<Affix> type objects or definition strings. Very useful
for masking complex internal library structures behind Pointer[Void] handles.
=item C<driver>
Optional. Explicitly select the parser driver. Values are C<'Clang'> or C<'Regex'>. If omitted, C<Affix::Wrap> attempts
to find the C<clang> executable and falls back to Regex if unavailable.
=back
=head1 METHODS
lib/Test2/Tools/Affix.pm view on Meta::CPAN
sub {
system('valgrind --version');
}
);
plan skip_all 'Valgrind is not installed' if $exit_code;
diag 'Valgrind v', ( $stdout =~ m/valgrind-(.+)$/ ), ' found';
diag 'Generating suppressions...';
my @cmd = (
qw[valgrind --leak-check=full --show-reachable=yes --error-limit=no
--gen-suppressions=all --log-fd=1], $^X, '-e',
sprintf <<'', ( join ', ', map {"'$_'"} sort { length $a <=> length $b } map { path($_)->absolute->canonpath } @INC ) );
use strict;
use warnings;
use lib %s;
use Affix;
no Test2::Plugin::ExitSummary;
use Test2::V0;
pass "generate valgrind suppressions";
done_testing;
lib/Test2/Tools/Affix.pm view on Meta::CPAN
}
sub stacktrace($blah) {
use Test2::Util::Table qw[table];
$blah ?
join "\n", table(
max_width => 120,
collapse => 1, # Do not show empty columns
header => [ 'function', 'path', 'line' ],
rows => [
map { [ $_->{fn}, ( defined $_->{dir} && defined $_->{file} ) ? join '/', $_->{dir}, $_->{file} : '', $_->{line} // '' ] } @$blah
],
) :
'';
}
sub parse_xml {
my ($xml) = @_;
my $hash = {};
my $re = qr{<([^>]+)>\s*(.*?)\s*</\1>}sm;
while ( $xml =~ m/$re/g ) {
lib/Test2/Tools/Affix.pm view on Meta::CPAN
}
# Function to run anonymous sub in a new process with valgrind
sub leaks( $name, $code_ref ) {
init_valgrind();
#
require B::Deparse;
CORE::state $deparse //= B::Deparse->new(qw[-l]);
my ( $package, $file, $line ) = caller;
my $source = sprintf
<<'', ( join ', ', map {"'$_'"} sort { length $a <=> length $b } grep {defined} map { my $dir = path($_); $dir->exists ? $dir->absolute->realpath : () } @INC, 't/lib' ), Test2::API::test2_stack()->top->{count}, $deparse->coderef2text(...
use lib %s;
use Test2::V0 -no_srand => 1, '!subtest';
use Test2::Util::Importer 'Test2::Tools::Subtest' => ( subtest_streamed => { -as => 'subtest' } );
use Test2::Plugin::UTF8;
no Test2::Plugin::ExitSummary; # I wish
use Test2::Tools::Affix;
# Test2::API::test2_stack()->top->{count} = %d;
$|++;
my $exit = sub {use Affix; Affix::set_destruct_level(3); %s;}->();
# Test2::API::test2_stack()->top->{count}++;
t/012_enum.t view on Meta::CPAN
is $c->{LOGIC_OR}, 0, 'Logic OR';
is $c->{CMP_LESS}, 1, 'Comparison <';
};
subtest 'Passing Strings as Enum Values' => sub {
ok typedef( Fruit => Enum [ [ APPLE => 1 ], [ BANANA => 2 ], [ CHERRY => 3 ] ] ), 'typedef Fruit Enum';
affix $lib, 'identify_fruit', [ Fruit() ] => Int;
is identify_fruit( APPLE() ), 1, 'Passed constant APPLE (1)';
is identify_fruit( BANANA() ), 2, 'Passed constant BANANA (2)';
# This is the feature: passing the string name of the enum element
is identify_fruit('APPLE'), 1, 'Passed string "APPLE" -> maps to 1';
is identify_fruit('BANANA'), 2, 'Passed string "BANANA" -> maps to 2';
is identify_fruit('CHERRY'), 3, 'Passed string "CHERRY" -> maps to 3';
like warning { identify_fruit('DURIAN') }, qr[numeric], 'Unknown string "DURIAN" for enum';
};
subtest 'Dualvar Roundtrip' => sub {
affix $lib, 'get_fruit', [Int] => Fruit();
my $f = get_fruit(2);
is 0 + $f, 2, 'Numeric value is 2';
is "$f", 'BANANA', 'String value is "BANANA"';
# Passing the returned dualvar back to C
is identify_fruit($f), 2, 'Passed dualvar $f back to C';
t/013_callbacks.t view on Meta::CPAN
int a, double b, int c, double d, int e, double f, int g, double h,
const char* i, int* j
);
DLLEXPORT int call_kitchen_sink(kitchen_sink_cb cb) {
int j_val = 100;
cb(1, 2.0, 3, 4.0, 5, 6.0, 7, 8.0, "kitchen sink", &j_val);
return j_val + 1;
}
typedef int (*IntMap)(int);
DLLEXPORT int map_int(int val, IntMap cb) {
return cb(val);
}
typedef void (*RectInspector)(Rect*);
DLLEXPORT void inspect_rect(Rect* r, RectInspector cb) {
cb(r);
}
typedef Point (*PointGenerator)(void);
DLLEXPORT int check_point_gen(PointGenerator cb) {
t/013_callbacks.t view on Meta::CPAN
is $h, 8.0, 'Callback arg 8 (double)';
is $i, 'kitchen sink', 'Callback arg 9 (string)';
is $$j_ref, 100, 'Callback arg 10 (int*)';
$$j_ref = 200;
};
is $harness->($callback_sub), 201, 'return value';
};
subtest simple => sub {
typedef Point => Struct [ x => Int, y => Int ];
typedef Rect => Struct [ top_left => Point(), bottom_right => Point(), label => Array [ Char, 16 ] ];
isa_ok my $map = wrap( $lib_path, 'map_int', [ Int, Callback [ [Int] => Int ] ] => Int ), ['Affix'];
my $res = $map->(
10,
sub {
my $v = shift;
return $v * 2;
}
);
is $res, 20, 'Simple callback executed';
#
isa_ok my $inspect = wrap( $lib_path, 'inspect_rect', [ Pointer [ Rect() ], Callback [ [ Pointer [ Rect() ] ] => Void ] ] => Void ), ['Affix'];
my $r = { top_left => { x => 1, y => 1 }, bottom_right => { x => 2, y => 2 }, label => "Check" };
t/018_sv_type.t view on Meta::CPAN
}
}
// Returns a new SV* (Mortal)
DLLEXPORT SV* make_sv(int val) {
return sv_2mortal(newSViv(val));
}
END
# Test Argument Passing (SV)
# The signature "SV" maps to "SV*" in C because Affix detects the "SV" type name
isa_ok my $inc = wrap( $lib, 'inc_sv', [ Pointer [SV] ] => Void ), ['Affix'];
my $val = 10;
$inc->($val);
is $val, 11, 'Passed SV to C, modified in place';
# Test Return Value (SV)
isa_ok my $make = wrap( $lib, 'make_sv', [Int] => Pointer [SV] ), ['Affix'];
my $res = $make->(42);
is $res, 42, 'Received SV from C';
t/027_thread_safety.t view on Meta::CPAN
diag $str;
};
#
subtest ithreads => sub {
skip_all 'No ithreads', 1 unless $Config{useithreads};
require threads;
# Test core affix cloning and usage across threads
ok affix libc(), [ abs => 'absolute' ], [Int] => Int;
is absolute(-42), 42, 'Main thread: absolute(-42) == 42';
my @threads = map {
threads->create(
sub {
my $tid = threads->tid();
my $res = absolute( -100 * $tid );
return $res == ( 100 * $tid );
}
)
} 1 .. 5;
for my $thr (@threads) {
my $tid = $thr->tid();