Sanko Robinson

Human.

/ /
· · · ·
Less
More
sanko/infix v0.1.5 C

[0.1.5] - 2026-02-06

This release expands platform stability. Focus was on SEH and DWARF unwinding, float16 and AVX-512 vector types.

Added

  • Added support for half-precision floating-point (float16).
  • Implemented C++ exception propagation through JIT frames on Linux (x86-64 and ARM64) using manual DWARF .eh_frame generation and __register_frame.
  • Implemented Structured Exception Handling (SEH) for Windows x64 and ARM64 for C++ exception propagation through trampolines.
  • Added infix_forward_create_safe API to establish an exception boundary that catches native exceptions and returns a dedicated error code (INFIX_CODE_NATIVE_EXCEPTION).
  • Added support for 256-bit (AVX) and 512-bit (AVX-512) vectors in the System V ABI.
  • Added support for receiving bitfield structs in reverse call trampolines.
  • Added trampoline caching. Identical signatures and targets now share the same JIT-compiled code and metadata via internal reference counting, significantly reducing memory overhead and initialization time.
  • Added a new opt-in build mode (--sanity) that emits extra JIT instructions to verify stack pointer consistency around user-provided marshaller calls, making it easier to debug corrupting language bindings.
  • Added t/405_simd_vectors_avx.c, t/406_simd_forward.c, t/407_float16.c, and t/902_cache_deduplication.c to the test suite.

Changed

  • Unified platform and compiler detection macros into a consistent INFIX_* namespace (e.g., INFIX_OS_LINUX, INFIX_ARCH_X64, INFIX_COMPILER_GCC) throughout the codebase.
  • Updated infix_executable_make_executable and internal layout structures to track trampoline prologue sizes, necessary for SEH and DWARF registration.
  • Explicitly enabled 16-byte stack alignment in Windows x64 trampolines to ensure SIMD compatibility.
  • Updated infix_type_create_vector to use the vector's full size for its natural alignment (e.g., 32-byte alignment for __m256).
  • 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 XMM0.
  • 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.
  • 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.
  • Improve C++ Mangling Support:
    • Itanium (GCC/Clang): Implemented full substitution support (S_, S0_, etc.) for complex or repeated types.
    • MSVC: Implemented type back-references (0-9) for parameter lists.

Fixed

  • Corrected an ARM64 bug in emit_arm64_ldr_vpr and emit_arm64_str_vpr where boolean conditions were being passed instead of actual byte sizes, causing data truncation for floating-point values in the direct marshalling path.
  • Fixed MSVC ARM: SEH XDATA layout to follow the architecture's specification exactly, enabling reliable exception handling on Windows on ARM.
  • Hardened instruction cache invalidation on ARM64 Linux/BSD with a robust manual fallback using assembly (dc cvau, ic ivau, etc.), ensuring generated code is immediately visible to the CPU.
  • Fixed the DWARF .eh_frame generation for ARM64 Linux FORWARD trampolines, correcting the instruction sequence and offsets to enable reliable C++ exception propagation.
  • Corrected a performance issue on x64 by adding vzeroupper calls in epilogues when AVX instructions are potentially used, avoiding transition penalties.
  • Fixed bitfield parsing logic to correctly handle colons in namespaces vs bitfield widths.
  • Fixed missing support for 256-bit and 512-bit vectors in System V reverse trampolines.
  • Rewrote _layout_struct in src/core/types.c to correctly handle bitfields larger than 8 bits and ensures bit_offset is always within the correct byte, matching standard C (well, GNU) compiler packing behavior.
  • Fixed a bug in the SysV recursive classifier that was incorrectly applying strict natural alignment checks to bitfield members. This was causing structs containing bitfields to be unnecessarily passed on the stack instead of in registers.

Full Changelog: https://github.com/sanko/infix/compare/v0.1.4...v0.1.5

sanko/Affix.pm v1.0.6 Perl

[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. On FreeBSD, SHM_ANON is now used.
  • [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 executable code by finding the stale RW pointer.
  • [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 shutdown. 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, overwriting adjacent stack memory.
  • [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.
  • [infix] Fixed potential cache coherency issues on Windows x64. The library now unconditionally calls FlushInstructionCache after JIT compilation.
  • [infix] Capped the maximum alignment in infix_type_create_packed_struct to 1MB to prevent integer wrap-around bugs in layout calculation.
  • [infix] Fixed a buffer overread on macOS ARM64 where small signed integers were loaded using 32-bit LDRSW. Implemented LDRSH and LDRSB.
  • [infix] Added native support for Apple's Hardened Runtime security policy.
    • The JIT engine now utilizes MAP_JIT when the com.apple.security.cs.allow-jit entitlement is detected.
    • Implemented thread-local permission toggling via pthread_jit_write_protect_np to maintain W^X compliance.

Full Changelog: https://github.com/sanko/Affix.pm/compare/v1.0.5...v1.0.6

sanko/infix v0.1.4 C

[0.1.4] - 2026-01-17

This release focuses on SIMD vector support and critical platform-specific stability fixes for macOS (both Intel and Apple Silicon), and improving internal code hygiene.

Added

  • Added infix_get_version and infix_version_t to the public API. This allows applications to query the semantic version of the library at runtime.
  • Added infix_registry_clone to support deep-copying type registries for thread-safe interpreter cloning.
  • Added t/404_simd_vectors.c: new unit tests for 128-bit SIMD vectors.
    • Currently targeting reverse callbacks to verify correct register marshalling.
  • Introduced INFIX_API and INFIX_INTERNAL macros to explicitly control symbol visibility.

Changed

  • 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. On FreeBSD, SHM_ANON is now used.
  • 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 executable code by finding the stale RW pointer.
  • The library now builds with hidden symbol visibility by default on supported compilers (GCC/Clang). Only public API functions (infix_*) are exported. Internal functions (_infix_*) are now hidden, preventing symbol collisions and ABI leakage when infix is linked statically into a shared library.
  • 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 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, overwriting adjacent stack memory.
  • 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)).
  • 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.
  • Fixed the build system (build.pl) to better handle external tool failures. Coverage gathering commands (like codecov) are now allowed to fail gracefully without breaking the build pipeline.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • Fixed Clang coverage reporting by switching from LLVM-specific profiles to standard GCOV formats.
  • Fixed potential cache coherency issues on Windows x64. The library now unconditionally calls FlushInstructionCache after JIT compilation.
  • Hardened the signature parser against integer overflows when parsing array/vector sizes.
  • Capped the maximum alignment in infix_type_create_packed_struct to 1MB to prevent integer wrap-around bugs in layout calculation.
  • Fixed a buffer overread on macOS ARM64 where small signed integers were loaded using 32-bit LDRSW. Implemented LDRSH and LDRSB.
  • Updated the INFIX_NODISCARD macro logic in infix.h. It now prioritizes compiler-specific attributes (like __attribute__((warn_unused_result))) over C23 standard attributes on GCC/Clang when not in strict C23 mode. This fixes syntax errors when compiling with C11/C17 standards.
  • Fixed warn_unused_result warnings across the test suite and fuzzing helpers. Previous tests cast ignored return values to (void), but GCC ignores this cast for functions marked with warn_unused_result. All tests now properly check infix_status return codes.
  • Fixed a "dangling else" warning in fuzz/fuzz_helpers.c by adding explicit braces.
  • Cleaned up infix_internals.h by removing the obsolete declaration for _infix_forward_create_internal.
  • Made _infix_forward_create_impl and _infix_forward_create_direct_impl static in trampoline.c, as they are only used within that translation unit in the unity build.
  • Early attempt to support C++ Itanium (INFIX_DIALECT_ITANIUM_MANGLING) and MSVC (INFIX_DIALECT_MSVC_MANGLING) mangling in signature stringification. This is mostly for debugging the type system quickly.
    • Supports deeply nested namespaces and scopes (@Outer::Inner::MyClass).
    • MSVC mangling correctly handles the reversed-order namespace requirements and special type prefixes (U for structs, T for unions).
  • Added native support for Apple's Hardened Runtime security policy.
    • The JIT engine now utilizes MAP_JIT when the com.apple.security.cs.allow-jit entitlement is detected.
    • Implemented thread-local permission toggling via pthread_jit_write_protect_np to maintain W^X compliance.
C++ Exception Unwinding
1 min 2

I've put it off for months but now have unit tests that sorta kinda demonstrates it. [Edit: I have no roadmap or timeline but] I'll need to come back to this before I try to wrap the C++ based SFML with Affix.

Chapter 39: Absolute Power with Assembly and Perl
3 min

C is often called "portable assembly," but sometimes you need the real thing.

If you need to read the CPU Time Stamp Counter (RDTSC) for nanosecond-precision benchmarking, access specific processor flags, or utilize SIMD instructions not exposed by your C compiler, you can write raw assembly and call it directly from Perl.

The Recipe

This time, we'll implement a function to read the CPU cycle count (RDTSC) on x86-64 Linux/Unix/Windows systems.

use v5.40;
use Affix qw[:all];
use Affix::Build;

# 1. Write Assembly
# We use AT&T syntax (standard for GCC/Clang on Unix).
# This file (.s) will be passed to the assembler.
my $c = Affix::Build->new( name => 'asm_lib' );
$c->add( \<<~'END', lang => 's' );
    .text
    .global get_ticks

    get_ticks:
        # rdtsc puts 32 bits in EDX (high) and 32 bits in EAX (low)
        rdtsc

        # We need to pack them into a single 64-bit register (RAX)
        # Shift RDX left by 32 bits
        shl $32, %rdx

        # Combine RDX and RAX
        or %rdx, %rax

        # Return value is in RAX
        ret
END

# 2. Link
# Note: On macOS, symbols need a leading underscore ('_get_ticks').
my $lib = $c->link;

# 3. Bind
# The function takes no args and returns a 64-bit unsigned integer.
affix $lib, 'get_ticks', [] => UInt64;

# 4. Benchmark
say 'Measuring CPU cycles for a Perl operation...';
my $start = get_ticks();

# Do some work
my $x = 0;
$x += $_ for 1 .. 1000;

# Check the clock again
my $end = get_ticks();
say 'Start: ' . $start;
say 'End:   ' . $end;
say 'Diff:  ' . ( $end - $start ) . ' cycles';

On my system, the output looks like this:

Measuring CPU cycles for a Perl operation...
Start: 4098233110110909
End:   4098233110188467
Diff:  77558 cycles

How It Works

  • 1. The Assembler When you pass lang => 's', Affix::Build invokes the system assembler (usually as or gcc). It compiles the raw instructions into machine code.

  • 2. The ABI You must respect the Calling Convention of your platform (System V AMD64 in this example).

    • Arguments: RDI, RSI, RDX...
    • Return Values: RAX
    • Safety: You must preserve "callee-saved" registers (RBX, RBP, R12-R15) if you use them. Since rdtsc only clobbers RAX and RDX (which are caller-saved/volatile), we don't need to push/pop anything.

Kitchen Reminders

  • Architecture Specific Assembly is not portable. This code will crash on ARM64 (Apple Silicon, Raspberry Pi) or 32-bit systems. You must detect $Config{archname} and provide different assembly implementations for different CPUs.

  • Syntax Flavors

    • Unix/Linux: Uses AT&T syntax (%reg, src, dest).
    • Windows: Uses Intel syntax (reg, dest, src) via NASM. If you are targeting Windows and Intel, you should use lang => 'asm' (NASM) instead of 's'.
Chapter 38: Scientific Speed with Fortran and Perl
3 min

If you are doing heavy scientific computing, you will eventually encounter Fortran (BLAS, LAPACK).

Binding Fortran can be tricky because:

  1. Name Mangling: Compilers often lowercase names and append underscores (e.g., DGEMM becomes dgemm_).
  2. Pass-by-Reference: Historically, Fortran passes everything by reference (pointers).
  3. Strings: Legacy Fortran expects string pointers to be followed by hidden length arguments at the end of the argument list.

However, modern Fortran (2003+) offers the iso_c_binding module, which allows us to define a standard C ABI.

The Recipe

This time, we'll compile a Fortran subroutine that adds two numbers, and another that prints a string based on an explicit length argument.

use v5.40;
use Affix qw[:all];
use Affix::Build;
$|++;

# 1. Compile Fortran
# Requires a Fortran compiler be installed
my $c = Affix::Build->new( name => 'fortran_demo' );
$c->add( \<<~'END', lang => 'f90' );
        ! Numeric subroutine: Passes pointers by default
        subroutine f_add(a, b, res) bind(c, name='f_add')
            use iso_c_binding
            integer(c_int), intent(in) :: a, b
            integer(c_int), intent(out) :: res
            res = a + b
        end subroutine

        ! String subroutine: Explicit length handling
        subroutine f_hello(msg, len) bind(c, name='f_hello')
            use iso_c_binding
            character(kind=c_char, len=1), intent(in) :: msg(*)
            integer(c_int), value :: len

            ! Print the slice using the passed length
            print *, "Fortran says: ", msg(1:len)
        end subroutine
    END
my $lib = $c->link;

# 2. Bind
# Modern Fortran with bind(c) respects C names.
# However, Fortran arguments are usually pointers.
affix $lib, 'f_add', [ Pointer [Int], Pointer [Int], Pointer [Int] ] => Void;

# For strings, we pass the buffer (String) and the length (Int) explicitly.
affix $lib, 'f_hello', [ String, Int ] => Void;

# 3. Call (Math)
# We must pass references (\$) to our numbers because Fortran expects pointers.
my $a   = 10;
my $b   = 20;
my $res = 0;
f_add( \$a, \$b, \$res );
say "10 + 20 = $res";    # 30

# 4. Call (Strings)
my $msg = 'Hello World';

# We pass the string string (char*), AND the length manually.
f_hello( $msg, length($msg) );

How It Works

  • 1. iso_c_binding This standard module is the key to sanity. By adding bind(c, name='f_add'), we force the Fortran compiler to generate a symbol named f_add (no trailing underscores) that uses the standard C calling convention.

  • 2. Pointers vs Values In f_add, the arguments a and b do not have the value attribute. This means Fortran expects memory addresses. In Perl, we handle this by passing references (\$a). In f_hello, len is defined with value. This means Fortran expects a raw integer, so we pass it directly from Perl.

Kitchen Reminders

  • Runtime Dependencies Shared libraries compiled with gfortran depend on libgfortran. On Linux, this is usually automatic. On Windows (MinGW), if you ship your compiled DLL to another machine, you must ensure libgfortran-*.dll is also in the PATH, or the library will fail to load.

  • Hidden Arguments If you are binding to legacy Fortran (BLAS/LAPACK) that was not written with iso_c_binding, you must account for "Hidden Arguments." For every string argument in the signature, you must usually append a Size_t argument to the very end of the argument list containing the string length.

Chapter 37: Oxidizing Perl (Binding to Rust)
4 min

Perl is great for glue; Rust is great for logic.

Historically, binding Rust to Perl required complex XS modules or specific crates like perl-xs. With Affix::Build, you can treat Rust just like C. By using standard ABI hooks (extern "C"), we can compile and run Rust code dynamically without ever leaving the Perl script.

The Recipe

Let's define a simple Rust library that operates on a Struct and handles strings.

use v5.40;
use Affix qw[:all];
use Affix::Build;
$|++;

# 1. Compile Rust
# Affix::Build detects the 'rs' extension and invokes `rustc`.
my $c = Affix::Build->new( name => 'rust_demo' );

# We define a struct to share between Rust and Perl
$c->add( \<<~'RUST', lang => 'rs' );
    use std::ffi::CStr;
    use std::os::raw::c_char;

    // We must define the struct layout to match C
    #[repr(C)]
    pub struct Point {
        x: i32,
        y: i32,
    }

    // #[no_mangle] keeps the symbol name "add_points"
    // extern "C" ensures the ABI uses standard registers
    #[no_mangle]
    pub extern "C" fn add_points(a: Point, b: Point) -> Point {
        Point {
            x: a.x + b.x,
            y: a.y + b.y
        }
    }

    // Example of reading a Perl string
    // We accept a raw pointer (*const c_char)
    #[no_mangle]
    pub unsafe extern "C" fn hello_rust(name: *const c_char) {
        if !name.is_null() {
            let c_str = CStr::from_ptr(name);
            if let Ok(s) = c_str.to_str() {
                println!("Hello, {} from Rust!", s);
            }
        }
    }
RUST
my $lib = $c->link;

# 2. Define Types
typedef Point => Struct [ x => Int, y => Int ];

# 3. Bind
# We treat Rust structs exactly like C structs.
affix $lib, 'add_points', [ Point(), Point() ] => Point();
affix $lib, 'hello_rust', [String]             => Void;

# 4. Execute
my $p1 = { x => 10, y => 20 };
my $p2 = { x => 5,  y => 5 };

# Pass by value
my $p3 = add_points( $p1, $p2 );
say "Result: $p3->{x}, $p3->{y}";    # 15, 25

# Pass string
hello_rust('Perl');

How It Works

  • 1. #[repr(C)] Rust creates its own memory layout for structs by default, which usually does not match C (or Affix). Adding this attribute forces the compiler to lay out the struct exactly like a C struct, ensuring Affix can write to fields x and y correctly.

  • 2. #[no_mangle] and extern "C" Just like C++, Rust mangles function names (e.g., _ZN7example3add...) to support namespaces and generics.

    • #[no_mangle] turns off name mangling, keeping the symbol as add_points.
    • extern "C" sets the calling convention to the system standard (cdecl/SystemV).
  • 3. Pointers and Unsafe When accepting pointers from Perl (like the string in hello_rust), Rust requires an unsafe block to dereference them. Affix handles the memory management of the string buffer (ensuring it is NULL-terminated); Rust just reads it.

Kitchen Reminders

  • Panics If your Rust code panics (crashes), it may "unwind" the stack across the FFI boundary. This is undefined behavior and will likely abort the Perl interpreter immediately. Pro Tip: Use std::panic::catch_unwind inside your extern "C" functions to catch Rust errors and return a clean error code to Perl instead of crashing.

  • Windows MinGW If you are using Strawberry Perl, Affix::Build automatically handles the ABI mismatch between Rust (which defaults to MSVC on Windows) and Perl (which uses GCC/MinGW), ensuring the libraries link correctly.

Chapter 36: Unlocking the C/C++ Ecosystem with Affix::Wrap and Alien::Xrepo
6 min

In previous chapters, we compiled code directly or used Alien::Base to find system libraries. But sometimes you want a library that isn't installed on the system, isn't on CPAN, but is available in the wider C/C++ ecosystem.

This is where xmake and its package manager, xrepo, shine.

xrepo is a modern, cross-platform C/C++ package manager. It can download, compile, and install thousands of libraries (like libpng, zlib, pcre, ffmpeg) with a single command.

We have created a helper module, Alien::Xrepo, to bridge the gap. It asks xrepo to install a library, finds the resulting shared object (.dll / .so), and hands it to Affix::Wrap for binding.

The Recipe

We will install libpng using Alien::Xrepo and use it to generate a gradient image file. We'll set up the script so you can easily define the size and color range. The current config produces a PNG that looks like this:

test_affix

use v5.40;
use Affix::Wrap;
use Alien::Xrepo;
use List::Util qw[min max];
use constant PI => 3.14159265359;

# Configuration
my $width  = 400;
my $height = 400;
my $angle  = 135;    # Degrees (0 = Horizontal, 90 = Vertical)

# Define gradient stops (Red -> Yellow -> Green -> Blue)
my @stops = (
    [ 255, 0,   0 ],     # Red
    [ 255, 255, 0 ],     # Yellow
    [ 0,   255, 0 ],     # Green
    [ 0,   0,   255 ]    # Blue
);

# 1. Initialize the Repo Helper
my $repo = Alien::Xrepo->new( verbose => 1 );

# 2. Install Library
# This runs `xrepo install -k shared libpng`
# It downloads source, compiles a DLL/SO, and returns the paths.
my $pkg = $repo->install('libpng');
say 'Found libpng version ' . $pkg->version . ' at ' . $pkg->libpath;

# 3. Locate Header
# We need the full path to png.h to parse function signatures.
my $header = $pkg->find_header('png.h');

# 4. Wrap
# We must pass include_dirs so Affix::Wrap's parser can find zlib (which libpng depends on).
my $wrapper = Affix::Wrap->new( project_files => [$header], include_dirs => $pkg->includedirs );

# We define a helper type that png.h relies on but doesn't define clearly for FFI
$wrapper->wrap( $pkg->libpath );

# 5. Use the Library
# We will use the high-level API of libpng to write a file.
# A. Generate Data (Math!)
my $buffer = '';

# Convert angle to vector
my $rad = $angle * PI / 180;
my $dx  = cos($rad);
my $dy  = sin($rad);

# Calculate projection bounds to normalize the gradient
# We project the 4 corners of the image onto the gradient vector
my @corners = ( 0 * $dx + 0 * $dy, $width * $dx + 0 * $dy, 0 * $dx + $height * $dy, $width * $dx + $height * $dy );
my $min_p   = min @corners;
my $max_p   = max @corners;
my $dist    = $max_p - $min_p || 1;         # Avoid division by zero
for my $y ( 0 .. $height - 1 ) {
    for my $x ( 0 .. $width - 1 ) {
        my $p = $x * $dx + $y * $dy;        # Project pixel onto gradient line
        my $t = ( $p - $min_p ) / $dist;    # Normalize to 0.0 - 1.0 range

        # Find which two colors we are blending between
        # Map t (0..1) to index (0 .. #stops-1)
        my $pos     = $t * $#stops;
        my $idx     = int $pos;       # Lower color index
        my $local_t = $pos - $idx;    # Blend factor between lower and upper

        # Clamp for safety at the very edge
        if ( $idx >= $#stops ) { $idx = $#stops - 1; $local_t = 1.0; }
        my $c1 = $stops[$idx];
        my $c2 = $stops[ $idx + 1 ];

        # Lerp
        my $r = int( $c1->[0] + ( $c2->[0] - $c1->[0] ) * $local_t );
        my $g = int( $c1->[1] + ( $c2->[1] - $c1->[1] ) * $local_t );
        my $b = int( $c1->[2] + ( $c2->[2] - $c1->[2] ) * $local_t );
        $buffer .= pack( 'CCC', $r, $g, $b );
    }
}

# B. Setup Struct
# Affix lets us pass a Perl HashRef for the C struct 'png_image'.
my $image = {
    version => PNG_IMAGE_VERSION(),    # Macro wrapped by Affix
    width   => $width,
    height  => $height,
    format  => PNG_FORMAT_RGB(),       # Macro wrapped by Affix
    flags   => 0,
    opaque  => undef
};
say 'Writing to test_affix.png...';

# C. Call Function
# int png_image_write_to_file(png_imagep image, const char *file, ...);
my $result = png_image_write_to_file(
    $image, 'test_affix.png', 0,    # convert_to_8bit (auto)
    $buffer,                        # Raw pixel data
    0,                              # row_stride (auto)
    undef                           # colormap
);
#
say $result ? 'SUCCESS: Image written.' : 'FAILURE: ' . $image->{message};

How It Works

  • $repo->install('libpng') This command invokes the external xrepo tool. It checks if libpng is cached. If not, it downloads the source, compiles it as a shared library (critical for FFI), and returns an object containing the paths to the binary and headers.

  • $pkg->includedirs Libraries often depend on other libraries (e.g., libpng needs zlib). xrepo handles this dependency tree. When we pass these directories to Affix::Wrap, we ensure that the Clang or Regex parser can find zlib.h when it parses png.h.

  • The Math While we use C for the I/O, we kept the pixel logic in Perl. We use a Linear Projection to calculate where every pixel falls along the angled line defined by $angle. We then map that position to our array of @stops and interpolate the RGB values. This demonstrates that Perl is more than capable of handling complex logic, while Affix handles the heavy lifting of interfacing with system libraries.

Why use Alien::Xrepo?

  1. Cross-Platform: xmake works on Windows, Linux, and macOS.
  2. Huge Repository: It supports thousands of libraries via conan, vcpkg, brew, and its own repository.
  3. Compilation: Unlike system package managers (apt/yum) which often only provide static .a files for development, xrepo can be forced to build .dll / .so files, which is exactly what dynamic FFI needs.

This approach gives you the power of CPAN's Alien:: namespace but with access to the entire C/C++ open source ecosystem instantly.

Chapter 35: Packaging an Affix::Build Module for CPAN with Module::Build
6 min

In the previous chapter, we built a script that compiles its own dependencies. To share this on CPAN, we want a standard module structure.

Most modern Perl developers use Module::Build to manage the installation of their distributions. Module::Build can build an XS based module automatically but has no idea how to build with Affix::Build. To work around this, we must create a custom builder class to hook into the build process and compile our C library exactly when the user runs ./Build.

We're going to use Minilla to mint our dists. A complete copy of this demo can be found here on github.

1. The Setup

Initialize your project:

minil new Acme-Image-Stb
cd Acme-Image-Stb
mkdir builder

2. Configuration (minil.toml)

We need to tell Minilla two things:

  1. Use Module::Build (instead of Module::Build::Tiny).
  2. Use our custom class (builder::MyBuilder) to drive the process.

Edit minil.toml:

name = "Acme-Image-Stb"
badges = ["github-actions/test.yml"]
module_maker="ModuleBuild"
static_install = "auto"

[build]
build_class = "builder::MyBuilder"

3. The Custom Builder (builder/MyBuilder.pm)

This is where the magic happens. We subclass Module::Build and override the ACTION_code method. This method runs when the user types ./Build.

We use Affix::Build to build the library and place it directly into the staging directory (blib/arch), ensuring it gets installed correctly.

package builder::MyBuilder;
use v5.40;
use parent 'Module::Build';
use Affix::Build;
use HTTP::Tiny;
use Path::Tiny;
use Config;

sub ACTION_code ($self) {
    unless ( defined $self->config_data('lib') ) {
        say 'Building embedded C library...';

        # Setup source directory
        my $src_dir = path('_src');
        $src_dir->mkpath;

        # Download headers if missing. In reality, you would probably just bundle the headers with the dist.
        # But this isn't reality.
        my $http = HTTP::Tiny->new;
        for my $file (qw(stb_image.h stb_image_resize2.h stb_image_write.h)) {
            my $path = $src_dir->child($file);
            next if $path->exists;
            say "  Fetching $file...";
            my $res = $http->get("https://raw.githubusercontent.com/nothings/stb/master/$file");
            die "Failed to download $file" unless $res->{success};
            $path->spew_raw( $res->{content} );
        }

        # Determine output path
        # We want the DLL to end up in: blib/arch/auto/Acme/Image/Stb/stb.so.xx.xx
        # This ensures it is installed in the architecture-specific library path.
        my $dist_name = $self->dist_name;    # "Acme-Image-Stb"
        my @parts     = split /-/, $dist_name;
        my $arch_dir  = path( $self->blib, 'arch', 'auto', @parts );
        $arch_dir->mkpath;

        # Compile with Affix
        my $c = Affix::Build->new(
            version   => $self->dist_version,
            name      => 'stb',
            build_dir => $arch_dir,
            flags     => { cflags => "-I$src_dir -O3", ldflags => ( $^O eq 'MSWin32' ? '-Wl,--export-all-symbols' : '' ) }
        );
        $c->add( \<<~'C', lang => 'c' );
        #if defined(_WIN32)
          #define STBIDEF __declspec(dllexport)
          #define STBIWDEF __declspec(dllexport)
          #define STBIRDEF __declspec(dllexport)
        #else
          #define STBIDEF __attribute__((visibility("default")))
          #define STBIWDEF __attribute__((visibility("default")))
          #define STBIRDEF __attribute__((visibility("default")))
        #endif

        #define STB_IMAGE_IMPLEMENTATION
        #define STB_IMAGE_WRITE_IMPLEMENTATION
        #define STB_IMAGE_RESIZE_IMPLEMENTATION

        #include "stb_image.h"
        #include "stb_image_resize2.h"
        #include "stb_image_write.h"
    C
        my $lib_file = $c->link;
        say "  Compiled: $lib_file";
        $self->config_data( lib => $lib_file->basename );
    }

    # Run standard build steps (copying .pm files to blib/)
    $self->SUPER::ACTION_code;
}
1;

4. Dependencies (cpanfile)

We need to add our build tools to the configure phase.

requires 'perl', '5.040';
requires 'Affix', 'v1.0.3';

on 'configure' => sub {
    requires 'Module::Build';
    requires 'HTTP::Tiny';
    requires 'Path::Tiny';
    requires 'Affix::Build', 'v1.0.3';
};

on 'test' => sub {
    requires 'Test2::V0';
};

5. The Module (lib/Acme/Image/Stb.pm)

Since we installed the library into blib/arch/auto/..., we can't just look in the current directory. We need to look where Perl installs architecture-specific files (XS modules usually live here).

We use a helper to scan @INC.

package Acme::Image::Stb 0.01 {
    use v5.40;
    use Affix qw[:all];
    use Carp  qw[croak];
    use Config;
    use Acme::Image::Stb::ConfigData;
    use parent 'Exporter';
    our %EXPORT_TAGS = ( internals => [qw[stbi_load stbi_write_png stbir_resize_uint8_linear]], core => [qw[load_and_resize]] );
    $EXPORT_TAGS{all} = [ our @EXPORT_OK = sort map {@$_} values %EXPORT_TAGS ];

    # Locate Library
    # We look for: .../auto/Image/Stb/stb.so (or .dll)
    my $lib_name = Acme::Image::Stb::ConfigData->config('lib');
    my $lib_path;
    for my $dir (@INC) {
        my $check = "$dir/auto/Acme/Image/Stb/$lib_name";
        if ( -e $check ) {
            $lib_path = $check;
            last;
        }
    }
    croak "Could not find compiled library '$lib_name' in \@INC" unless $lib_path;
    my $lib = Affix::load_library($lib_path);

    # Bindings
    use constant STBIR_RGBA => 4;
    affix $lib, 'stbi_load',                 [ String, Pointer [Int], Pointer [Int], Pointer [Int], Int ] => Buffer;
    affix $lib, 'stbi_write_png',            [ String, Int, Int, Int, Buffer, Int ]                       => Int;
    affix $lib, 'stbir_resize_uint8_linear', [ Buffer, Int, Int, Int, Buffer, Int, Int, Int, Int ]        => Buffer;

    # API
    sub load_and_resize ( $input, $output, $scale ) {
        my ( $w, $h, $ch ) = ( 0, 0, 0 );

        # Load
        my $img = stbi_load( $input, \$w, \$h, \$ch, 4 );
        return undef if is_null($img);

        # Calculate
        my $nw      = int( $w * $scale );
        my $nh      = int( $h * $scale );
        my $out_buf = "\0" x ( $nw * $nh * 4 );

        # Resize
        my $res = stbir_resize_uint8_linear( $img, $w, $h, 0, $out_buf, $nw, $nh, 0, STBIR_RGBA );
        return undef if is_null($res);

        # Save
        stbi_write_png( $output, $nw, $nh, 4, $out_buf, 0 );
    }
}
1;
__END__

=pod

=encoding utf-8

=head1 NAME

Acme::Image::Stb - It's new $module

=head1 SYNOPSIS

    use Acme::Image::Stb;

=head1 DESCRIPTION

Acme::Image::Stb is ...

=head1 LICENSE

Copyright (C) You.

This library might be free software; you may or may not be able to redistribute it
and/or modify it under the same terms of the author.

=head1 AUTHOR

You E<lt>yournamehere@cpan.orgE<gt>

=cut

6. Development Workflow

With this setup, the standard Minilla commands work perfectly, but now they trigger a C compiler in the background.

# Install dependencies
$ cpanm --installdeps .

# Build and test
$ minil test

# Make the dist
$ minil dist

Kitchen Reminders

By subclassing Module::Build, we have seamlessly integrated Affix::Build into the standard Perl toolchain.

  1. Transparency: Users installing via cpanm won't know you aren't using XS. It just works.
  2. Correctness: By targeting blib/arch/auto, we respect Perl's directory structure for binary binaries. This demo does it in a rather unsophisticated way...
  3. Flexibility: You can add logic to MyBuilder.pm to detect system libraries (via pkg-config or xrepo) and only compile the bundled version if the system version is missing.

Pat yourself on the back. You have now created a binary distribution ready for CPAN without writing a single line of XS.

sanko/Affix.pm Affix::Build and Affix::Wrap Perl

Based on infix v0.1.3

Added

  • Support for Variadic Functions (varargs):
    • Implemented dynamic JIT compilation for C functions with variable arguments (e.g., printf).
    • Added variadic_cache to cache trampolines for repeated calls, ensuring high performance.
    • Implemented runtime type inference: Perl integers promote to sint64, floats to double, and strings to *char.
  • Added Affix::coerce($type, $value) to explicitly hint types for variadic arguments. This allows passing structs by value or forcing specific integer widths where inference is insufficient.
  • Cookbook: I'm putting together chapters on a wide range of topics at https://github.com/sanko/Affix.pm/discussions/categories/recipes
  • affix and wrap functions now accept an address to bind to. This expects the library to be undef and jumps past the lib location and loading steps.
  • Added File and PerlIO types.
    • Allows passing Perl filehandles to C functions expecting standard C streams (PerlIO* => Pointer[PerlIO]).
    • Allows receiving FILE* from C and using them as standard Perl filehandles (FILE* => Pointer[File]).
  • A few new specialized pointer types:
    • StringList: Automatically marshals an array ref of strings to a null-terminated char** array (and back). This is useful in instances where argv or a similar list is expected.
    • Buffer: Allows passing a pre-allocated scalar as a mutable char* buffer to C (zero-copy write).
    • SockAddr: Safe marshalling of Perl packed socket addresses to struct sockaddr*.
  • Affix::Build: A polyglot shared library builder. Currently supports Ada, Assembly, C, C#, C++, Cobol, Crystal, Dlang, Eiffel, F#, Fortran, Futhark, Go, Haskell, Nim, OCaml, Odin, Pascal, Rust, Swift, Vlang, and Zig.
  • Affix::Wrap: An experimental tool to introspect C header files and generate Affix bindings and documentation.
    • Dual-Driver Architecture:
      • Affix::Wrap::Driver::Clang: Uses the system clang executable to parse the AST for high-fidelity extraction of types, macros, and comments.
      • Affix::Wrap::Driver::Regex: A zero-dependency fallback driver that parses headers using heuristics.

Changed

  • Array[Char] function arguments now accept Perl strings directly, copying the string data into the temporary C array.
  • Affix::errno() now returns a dualvar containing both the numeric error code (errno/GetLastError) and the system error string (strerror/FormatMessage).

Fixed

  • Correctly implemented array decay for function arguments on ARM and Win64. Array[...] types are now marshalled into temporary C arrays and passed as pointers, matching standard C behavior. Previously, they were incorrectly passed by value, causing stack corruption.
  • Fixed binary safety for Array[Char/UChar]. Reading these arrays now respects the explicit length rather than stopping at the first null byte.
  • The write-back mechanism no longer attempts to overwrite the read-only ArrayRef scalar with the pointer address.
  • Pointer[SV] is now handled properly as args, return values, and in callbacks. Reference counting is automatic to prevent premature garbage collection of passed scalars.
  • Shared libs written in Go spin up background threads (for GC and scheduling) that do not shut down cleanly when a shared library is unloaded. This often causes access violations on Windows during program exit. We attempt to work around this by detecting libs with the Go runtime and just... not unloading them.

What's Changed

Full Changelog: https://github.com/sanko/Affix.pm/compare/v1.0.2...v1.0.3

Chapter 34: The Zero-Dependency Image Processor
5 min

For our next trick, we'll tackle the most common headache in the C ecosystem: Dependency Hell. Usually, if you want to manipulate images, you need libpng, libjpeg, zlib, and headers for all of them installed on your system. If your user is on a different OS, your script fails. To avoid that, let's use Affix::Build to compile the famous stb single-header libraries.

The Recipe

Here we'll load a JPG/PNG, resize it to a thumbnail using high-quality linear scaling, and save it as a PNG.

In other words, we'll turn this:

avatar

...into this:

avatar_small

Simple enough.

This recipe demonstrates two advanced techniques:

  1. Output Parameters: Passing pointers to variables so C can populate them with data.
  2. Raw Buffers: Passing the memory address of Perl scalars to C for high-performance data manipulation.
use v5.40;
use Affix qw[:all];
use Affix::Build;
use HTTP::Tiny;
use Path::Tiny;
use Config;
$|++;

# 1. Setup Persistent Cache
# Like Inline::C, we keep artifacts in a local subdirectory.
my $build_dir = path('.')->child('_build');
$build_dir->mkpath;

# Calculate the expected library name (e.g., stb_lib.dll or libstb_lib.so)
my $lib_file = $build_dir->child( ( $^O eq 'MSWin32' ? '' : 'lib' ) . 'stb_lib.' . $Config{so} );

# 2. Check Cache
if ( $lib_file->exists ) {
    say "Using cached library: $lib_file";
}
else {
    say "Library not found. Building...";
    build_library($build_dir);
}

# 3. Bindings
my $lib = Affix::load_library($lib_file);

# stbir_pixel_layout enum
use constant STBIR_RGBA => 4;

# "Buffer" passes the raw memory address of a Perl string to C.
affix $lib, 'stbi_load',                 [ String, Pointer [Int], Pointer [Int], Pointer [Int], Int ] => Buffer;
affix $lib, 'stbi_write_png',            [ String, Int, Int, Int, Buffer, Int ]                       => Int;
affix $lib, 'stbir_resize_uint8_linear', [ Buffer, Int, Int, Int, Buffer, Int, Int, Int, Int ]        => Buffer;

# 4. The Application
my $input_file  = 'avatar.jpg';
my $output_file = 'avatar_small.png';

# Generate dummy input if needed
unless ( -e $input_file ) {
    say 'Creating dummy input file...';
    my $w      = 100;
    my $h      = 100;
    my $pixels = '';

    # Gradient Pattern
    for my $y ( 0 .. $h - 1 ) {
        for my $x ( 0 .. $w - 1 ) {
            $pixels .= pack( 'C4', int( $x * 2.5 ), int( $y * 2.5 ), 0, 255 );
        }
    }
    stbi_write_png( $input_file, $w, $h, 4, $pixels, $w * 4 );
}

# Step A: Load
my ( $w, $h, $ch ) = ( 0, 0, 0 );
say "Loading $input_file...";
my $img_data = stbi_load( $input_file, \$w, \$h, \$ch, 4 );
die 'Failed to load image.' if is_null($img_data);
say "Original: ${w}x${h} ($ch channels)";

# Step B: Resize
my $new_w      = int( $w / 2 );
my $new_h      = int( $h / 2 );
my $in_stride  = $w * 4;
my $out_stride = $new_w * 4;

# Allocate output memory (Buffer)
my $out_data = "\0" x ( $new_h * $out_stride );
say "Resizing to ${new_w}x${new_h}...";
my $res = stbir_resize_uint8_linear( $img_data, $w, $h, $in_stride, $out_data, $new_w, $new_h, $out_stride, STBIR_RGBA );
die 'Resize failed!' if is_null($res);

# Step C: Save
say "Saving to $output_file...";
stbi_write_png( $output_file, $new_w, $new_h, 4, $out_data, $out_stride );
say 'Done.';

# Subroutines
sub build_library($dir) {
    my $http = HTTP::Tiny->new;

    # Download headers if missing
    for my $file (qw(stb_image.h stb_image_resize2.h stb_image_write.h)) {
        my $path = $dir->child($file);
        next if $path->exists;
        say "Downloading $file...";
        my $url = "https://raw.githubusercontent.com/nothings/stb/master/$file";
        my $res = $http->get($url);
        die "Failed to download $file" unless $res->{success};
        $path->spew_raw( $res->{content} );
    }
    my $c = Affix::Build->new(
        name      => 'stb_lib',
        build_dir => $dir,
        flags     => { cflags => "-I$dir -O3", ldflags => ( $^O eq 'MSWin32' ? '-Wl,--export-all-symbols' : '' ) }
    );
    $c->add( \<<~'C', lang => 'c' );
        #if defined(_WIN32)
          #define STBIDEF __declspec(dllexport)
          #define STBIWDEF __declspec(dllexport)
          #define STBIRDEF __declspec(dllexport)
        #else
          #define STBIDEF __attribute__((visibility("default")))
          #define STBIWDEF __attribute__((visibility("default")))
          #define STBIRDEF __attribute__((visibility("default")))
        #endif

        #define STB_IMAGE_IMPLEMENTATION
        #define STB_IMAGE_WRITE_IMPLEMENTATION
        #define STB_IMAGE_RESIZE_IMPLEMENTATION

        #include "stb_image.h"
        #include "stb_image_resize2.h"
        #include "stb_image_write.h"
    C
    $c->compile_and_link;
}

How It Works

  • 1. The Buffer Type We defined Buffer type in Affix to handle passing an opaque pointer. Perl strings, when passed as a Pointer type, usually copy the data. However, for Pointer[Void], Affix optimizations kick in: it passes the raw address of the Perl scalar's memory. This allows stbir_resize to write directly into $out_data without any copy overhead.

  • 2. Output Parameters (int *width) When calling stbi_load, we pass references (\$w, \$h). Affix detects this, allocates integer pointers, passes them to C, and then updates your Perl variables with the values C wrote (the image dimensions).

  • 3. The Pixel Layout The stb_image_resize2 library requires us to specify the memory layout of the pixels. We pass STBIR_RGBA (4) to tell it that every 4 bytes represent Red, Green, Blue, and Alpha. If we passed 0 (default), it would treat the image as Grayscale, resulting in a garbled output.

Chapter 33: SIMD Vectors for Number Crunching
4 min

Standard Perl scalars are designed for flexibility, not raw math throughput. When you need to process millions of coordinates, pixels, or audio samples, you want SIMD (Single Instruction, Multiple Data).

Affix makes SIMD vectors (like __m128, __m256, float32x4_t) first-class citizens and properly manages support on both x84 and ARM64.

The Recipe

Let's write a hypothetical 3D math library that uses 128-bit vectors (4 floats).

use v5.40;
use Affix qw[:all];
use Affix::Build;

# 1. Compile C Library
# We use GCC/Clang vector extensions for this example.
my $c = Affix::Build->new( flags => { cflags => '-O3 -mavx' } );
$c->add( \<<~'END', lang => 'c', );
    #if defined(__GNUC__) || defined(__clang__)
        typedef float v4f __attribute__((vector_size(16)));

        // Add two vectors
        v4f add_vecs(v4f a, v4f b) {
            return a + b;
        }

        // Scale a vector by a scalar
        v4f scale_vec(v4f v, float s) {
            return v * s;
        }

        // Dot product (returns scalar)
        float dot_vec(v4f a, v4f b) {
            v4f res = a * b;
            return res[0] + res[1] + res[2] + res[3];
        }
    #endif
END
my $lib = $c->link;

# 2. Bind
# We define a generic "Vec4" type: v[4:float]
typedef Vec4 => Vector [ 4, Float ];
affix $lib, 'add_vecs',  [ Vec4(), Vec4() ] => Vec4();
affix $lib, 'scale_vec', [ Vec4(), Float ]  => Vec4();
affix $lib, 'dot_vec',   [ Vec4(), Vec4() ] => Float;

# 3. Use (Packed Strings - The Fast Way)
# Pack 4 floats into a 16-byte string
my $v1 = pack( 'f4', 1.0, 2.0, 3.0, 4.0 );
my $v2 = pack( 'f4', 5.0, 6.0, 7.0, 8.0 );

# Add
my $sum_ref = add_vecs( $v1, $v2 );

# Results come back as ArrayRefs by default (for convenience)
say 'Sum: ' . join ', ', @$sum_ref;    # 6, 8, 10, 12

# 4. Use (ArrayRefs - The Convenient Way)
# You can also pass ArrayRefs directly. Affix handles the packing.
my $scaled_ref = scale_vec( [ 1.0, 1.0, 1.0, 1.0 ], 0.5 );
say 'Scaled: ' . join( ', ', @$scaled_ref );    # 0.5, 0.5, 0.5, 0.5

# 5. Dot Product
my $dot = dot_vec( $v1, $v2 );
say 'Dot Product: ' . $dot;                     # 1*5 + 2*6 + 3*7 + 4*8 = 70

How It Works

  • 1. First-Class Types

    Vector[ 4, Float ]
    

    Affix understands that this is not just an array, but a specific machine type (128-bit register). On x64, this maps to __m128 and is passed in SSE registers (XMM0, etc.). On ARM64, it maps to NEON registers (V0, etc.).

  • 2. Packed String Input

    my $v1 = pack( 'f4', ... );
    

    This is the most efficient way to work. You are creating the raw binary layout of the vector in a Perl scalar. Affix simply passes the pointer to this data directly to the C function (or loads it into a register). This avoids the overhead of iterating over a Perl array and converting individual values.

  • 3. ArrayRef Output

    By default, Affix unpacks returned vectors into a Perl ArrayRef (e.g., [x, y, z, w]). This is convenient for debugging or light math.

Kitchen Reminders

  • Alignment

    SIMD instructions are picky about alignment. When you use pack, Perl usually handles the string buffer allocation, but it doesn't guarantee 16-byte alignment. Affix handles the move to the register safely. If you use malloc to store vectors for C functions, be sure to use an aligned allocation if the C library requires it.

  • Aliases

    Affix exports aliases for common SIMD types if you import :types:

    • M128 -> Vector[4, Float]
    • M128d -> Vector[2, Double]
    • M256 -> Vector[8, Float] (AVX)
Chapter 32: SIMD and Matrix Benchmarks
8 min

So, you read Chapters 22 and 23, but the ultimate question remains: "Is it fast?"

To find out, we'll benchmark Affix against the two titans of Perl performance:

  1. PDL (Perl Data Language) The standard for high-performance numerical computing in Perl. It is heavily optimized for processing large arrays ("piddles") in C.

  2. Inline::C The traditional way to write C extensions. It compiles C code on the fly and links it to Perl via XS. It is generally considered the speed limit of Perl.

Benchmark 1: SIMD Vector Addition

This set of benchmarks tests using 128-bit SIMD vectors (adding four floats at once):

  1. Micro-Benchmark: Call a function many times (measuring call overhead).
  2. Macro-Benchmark: Call a function once to process 100,000 items (measuring raw throughput).
use v5.40;
use Benchmark qw[cmpthese];
use Affix     qw[:all];
use Affix::Compiler;
use PDL;
use Inline C => Config => ( CCFLAGSEX => '-O3 -mavx' );
use Inline C => <<~'END_XS';
    typedef float v4f __attribute__((vector_size(16)));

    // Micro: Add single vector
    SV* inline_add(SV* a_sv, SV* b_sv) {
        STRLEN len_a, len_b;
        float* a = (float*)SvPVbyte(a_sv, len_a);
        float* b = (float*)SvPVbyte(b_sv, len_b);
        if (len_a < 16 || len_b < 16) croak("Bad length");
        v4f vc = *(v4f*)a + *(v4f*)b;
        return newSVpvn((const char*)&vc, 16);
    }

    // Macro: Add arrays of vectors
    void inline_bulk(SV* a_sv, SV* b_sv, SV* out_sv, int count) {
        v4f* a = (v4f*)SvPV_nolen(a_sv);
        v4f* b = (v4f*)SvPV_nolen(b_sv);

        SvUPGRADE(out_sv, SVt_PV);
        SvGROW(out_sv, count * 16 + 1);
        SvCUR_set(out_sv, count * 16);
        v4f* out = (v4f*)SvPV_nolen(out_sv);

        for(int i=0; i<count; i++) {
            out[i] = a[i] + b[i];
        }
    }
END_XS

# Compile C Library for Affix
my $c = Affix::Compiler->new( flags => { cflags => '-O3 -mavx' } );
$c->add( \<<~'END', lang => 'c' );
    typedef float v4f __attribute__((vector_size(16)));

    v4f affix_add(v4f a, v4f b) {
        return a + b;
    }

    void affix_bulk(v4f *a, v4f *b, v4f *out, int count) {
        for(int i=0; i<count; i++) {
            out[i] = a[i] + b[i];
        }
    }
END
my $lib = $c->link;

# Standard (Safe) Binding
my $std_add  = wrap $lib, 'affix_add',  [ Vector [ 4, Float ], Vector [ 4, Float ] ] => Vector [ 4, Float ];
my $std_bulk = wrap $lib, 'affix_bulk', [ Pointer [Void], Pointer [Void], Pointer [Void], Int ] => Void;

# Setup Data
# A = [1, 2, 3, 4], B = [5, 6, 7, 8] -> Sum = [6, 8, 10, 12]
my $v1      = pack( 'f4', 1, 2, 3, 4 );
my $v2      = pack( 'f4', 5, 6, 7, 8 );
my $p1      = pdl( [ 1, 2, 3, 4 ] );
my $p2      = pdl( [ 5, 6, 7, 8 ] );
my $count   = 100_000;
my $bytes   = $count * 16;
my $big_a   = $v1 x $count;
my $big_b   = $v2 x $count;
my $big_out = "\0" x $bytes;
my $big_p1  = $p1->dummy( 1, $count )->clump(2);
my $big_p2  = $p2->dummy( 1, $count )->clump(2);

# Helper to unpack Vector result if Affix returns ArrayRef
sub unpack_if_ref($val) {
    return ref($val) eq 'ARRAY' ? pack( 'f4', @$val ) : $val;
}

# Test 1: Micro-Benchmark
say 'Micro-Benchmark (Single Vector Add)';
cmpthese(
    -5,
    {   PDL         => sub { my $r = $p1 + $p2; },
        'Inline::C' => sub { inline_add( $v1, $v2 ) },
        Affix       => sub { $std_add->( $v1, $v2 ) }
    }
);

# Test 2: Macro-Benchmark
say 'Macro-Benchmark (100k Vectors / 1.6MB)';
cmpthese(
    -5,
    {   PDL         => sub { my $r = $big_p1 + $big_p2; },
        'Inline::C' => sub { inline_bulk( $big_a, $big_b, $big_out, $count ) },
        Affix       => sub { $std_bulk->( \$big_a, \$big_b, \$big_out, $count ) }
    }
);

The Results

Micro-Benchmark (Single Vector Add)
               Rate       PDL Inline::C     Affix
PDL        623532/s        --      -92%      -92%
Inline::C 7706580/s     1136%        --       -3%
Affix     7933356/s     1172%        3%        --
Macro-Benchmark (100k Vectors / 1.6MB)
             Rate       PDL Inline::C     Affix
PDL        1436/s        --      -94%      -94%
Inline::C 23291/s     1522%        --       -1%
Affix     23473/s     1535%        1%        --

Analysis

  1. Micro (Latency): Affix Wins by a Nose Affix is 3% faster than compiled XS (Inline::C) and more than 10x faster than PDL for small operations. This result is remarkable. Inline::C compiles static C code, but the XS macro system (SvPV, newSV) adds overhead. Affix generates a custom JIT trampoline at runtime that accesses the Perl stack and CPU registers directly, effectively behaving like inline assembly for Perl.

  2. Macro (Throughput): Virtual Tie In the bulk data test, Affix and Inline::C perform identically. This confirms that Affix has zero overhead once the pointer is passed to C.


Benchmark 2: Matrix Math (PDL Logic)

PDL is famous for its terse syntax and "broadcasting" capabilities. A classic example from the PDL synopsis is filling a matrix based on its coordinates and summing the results.

Before we race, we will verify that our C implementation produces the exact same results as PDL.

The Logic: $y = $x + 0.1 * xvals($x) + 0.01 * yvals($x)

use v5.40;
use Benchmark qw[cmpthese];
use Affix     qw[:all];
use Affix::Compiler;
use PDL;

# 1. Compile C Matrix Logic
my $c = Affix::Compiler->new( flags => { cflags => '-O3' } );
$c->add( \<<~'END', lang => 'c' );
    void calc_matrix(double *out, int rows, int cols) {
        for(int y=0; y < rows; y++) {
            for(int x=0; x < cols; x++) {
                int i = y * cols + x;
                // logic: 0 + 0.1*x + 0.01*y
                out[i] = 0.1 * x + 0.01 * y;
            }
        }
    }
END
my $mat_lib = $c->link;
my $calc    = wrap $mat_lib, 'calc_matrix', [ Pointer [Void], Int, Int ] => Void;

# 2. Setup (1000x1000 Matrix)
my $N     = 1000;
my $p_mat = zeroes $N, $N;
my $c_buf = "\0" x ( $N * $N * 8 );    # 8 bytes per double

# 3. Verification
say "\nVerifying results...";

# Run PDL Logic
my $p_res = $p_mat + 0.1 * xvals($p_mat) + 0.01 * yvals($p_mat);

# Run Affix Logic
$calc->( \$c_buf, $N, $N );

# Compare value at specific coordinate [500, 500]
# Expected: 0.1*500 + 0.01*500 = 50 + 5 = 55
my $idx     = 500 * $N + 500;
my $val_c   = unpack( 'd', substr( $c_buf, $idx * 8, 8 ) );
my $val_pdl = $p_res->at( 500, 500 );
if ( abs( $val_c - $val_pdl ) < 1e-9 ) {
    say "Verification Successful: Matches at [500, 500] ($val_c)";
}
else {
    die "Mismatch! C=$val_c PDL=$val_pdl";
}

# 4. Benchmark
say "Matrix Benchmark (1000x1000)";
cmpthese(
    -5,
    {   PDL => sub {
            my $y = $p_mat + 0.1 * xvals($p_mat) + 0.01 * yvals($p_mat);
        },
        Affix => sub {

            # The raw C logic via Affix
            $calc->( \$c_buf, $N, $N );
        }
    }
);

The Results

Verifying results...
Verification Successful: Matches at [500, 500] (55)
Matrix Benchmark (1000x1000)
        Rate   PDL Affix
PDL   78.5/s    --  -99%
Affix 6795/s 8555%    --

Analysis

In this specific case, Affix is nearly 9x faster than PDL.

While PDL is highly optimized C, it still has to allocate temporary objects for xvals, yvals, and the intermediate multiplication results before summing them. The custom C function loaded by Affix does the calculation in a single pass over the memory with no intermediate allocations, allowing the CPU to cache lines efficiently.

Summary

  • Speed: Affix is consistently the fastest way to bridge Perl and C, beating XS in call overhead and beating generic numerical libraries in raw throughput via custom C implementations.
  • Simplicity: We achieved this performance with one line of Perl (affix), versus lines of XS macros or complex PDL threading logic.
  • Portability: Affix requires no compilation at install time. You can ship your script without a C compiler dependency (if binding to pre-compiled libraries) and still get maximum performance.

Of course, none of these points matter if you can't write the C or Fortran to emulate the features of PDL.

Chapter 31: Wrapping C++ Classes by Hacking Vtables
5 min

In Chapter 30, we wrapped C++ classes using extern "C" helper functions in a shim. That is the 'Right Way.' But what if you are stuck with a pre-compiled C++ library and can't recompile it? What if you're just super bored? Let's say you have an object pointer, but no C functions to pass it to.

To call a method, we must act like a C++ compiler: manually look up the Virtual Method Table (vtable) and call the function pointer directly.

The Recipe

You know the drill. Let's create a C++ class, then write a Perl script that "scans" the object to find and call its methods.

use v5.40;
use Affix qw[:all];
use Affix::Build;
use Config;

# Compile C++ Library
# We define a class with virtual methods.
my $c = Affix::Build->new();
$c->add( \<<~'END', lang => 'cpp' );
    #include <stdio.h>

    class Calculator {
    public:
        // Explicit virtual destructor
        // (Often occupies vtable slots 0 & 1 on GCC/Clang)
        virtual ~Calculator() {}

        // Target Methods
        virtual void say_hello(const char* name) {
            printf("Hello, %s!\n", name);
        }

        virtual int add(int a, int b) {
            return a + b;
        }
    };

    extern "C" Calculator* new_calc() {
        return new Calculator();
    }
END
my $lib = $c->link;

# 2. Get the Object
affix $lib, 'new_calc', [] => Pointer [Void];
my $this = new_calc();

# Find the VTable
# The first 8 bytes (on 64-bit) of a C++ object are usually the vptr.
# Structure: $this -> [ vptr ] -> [ func0, func1, func2 ... ]
# Cast $this to a Pointer to a Size_t (Pointer-sized integer).
# This allows us to read the address stored in the first 8 bytes.
my $vptr_ref = cast( $this, Pointer [Size_t] );

# Dereference to get the address of the vtable array.
my $vtable_addr = $$vptr_ref;
say sprintf 'Object: 0x%X', address($this) ;
say sprintf 'VTable: 0x%X', $vtable_addr ;

# Scan the VTable
# We interpret the vtable as an array of 5 pointers (Size_t).
my $slots_pin = cast( $vtable_addr, Array [ Size_t, 5 ] );
my $addrs     = $$slots_pin;

# Dump the slots to find our functions.
# Code pointers will look like valid memory addresses.
# Slots 0/1 are often destructors or RTTI info.
for my $i ( 0 .. 4 ) {
    say sprintf( "Slot [%d]: 0x%X", $i, $addrs->[$i] );
}

# Heuristic:
# GCC/Clang (Itanium ABI): Slots 0,1 are dtors. Methods start at 2.
# MSVC (Windows ABI): Methods usually start at 0 or 1.
my ( $idx_hello, $idx_add ) = ( 2, 3 );

# Simple detection for MSVC
if ( $^O eq 'MSWin32' && $Config{cc} =~ /cl/i ) {
    ( $idx_hello, $idx_add ) = ( 0, 1 );
}
my $addr_hello = $addrs->[$idx_hello];
my $addr_add   = $addrs->[$idx_add];

# Bind and Call
# wrap() can take a raw integer address instead of a library handle.
#
# CRITICAL: C++ methods pass 'this' as the hidden first argument!
# We must include Pointer[Void] at the start of the signature.
if ( $addr_hello && $addr_add ) {
    my $say_hello = wrap( $addr_hello, [ Pointer [Void], String ] => Void );
    my $add       = wrap( $addr_add,   [ Pointer [Void], Int, Int ] => Int );

    # Execute
    $say_hello->( $this, 'Perl Hacker' );
    say '10 + 20 = ' . $add->( $this, 10, 20 );
}
else {
    say 'Could not resolve function addresses.';
}

The output looks like this:

Object: 0x11ECCA01160
VTable: 0x7FFBF85E4360
Slot [0]: 0x7FFBF85E2E40
Slot [1]: 0x7FFBF85E2E10
Slot [2]: 0x7FFBF85E2DC0
Slot [3]: 0x7FFBF85E2DA0
Slot [4]: 0x694D28203A434347
10 + 20 = 30
Hello, Perl Hacker!

How It Works

  • 1. The Memory Layout

    $this (0x1000)
    .-----------.
    | vptr      | ------> vtable (0x5000)
    +-----------+        +--------------------------+
    | member... |        | [0] Destructor           |
     `---------'         | [1] Destructor (Deleting)|
                         | [2] say_hello()   <-- TARGET
                         | [3] add()         <-- TARGET
                         +--------------------------+
    
  • 2. Double Indirection

    To get the function address, we must dereference twice:

    1. Read memory at $this to get $vtable_addr.
    2. Read memory at $vtable_addr + offset to get the function address.

    We achieve this in Affix by casting $this to Pointer[Size_t] to read the pointer-sized integer at that location.

  • 3. Wrapping Raw Pointers

    my $sub = wrap( $address, [Args] => Ret );
    

    The wrap function usually takes a library handle. However, if you pass a raw integer address (or a Pin) as the first argument, Affix treats it as the function address itself. This converts the raw vtable entry into a callable Perl subroutine.

Kitchen Reminders

  • Fragility

    This relies on the Itanium C++ ABI (Linux/macOS/MinGW) or the MSVC ABI (Visual Studio). The layout changes if you use Multiple Inheritance or Virtual Inheritance. This recipe works best for simple classes or COM-style interfaces (which use pure virtual classes).

  • The 'this' Argument

    C++ methods are just C functions with a hidden first argument (this). When binding manually, you must include this argument in the signature (Pointer[Void]) and pass the object instance every time you call the method.

Chapter 30: Wrapping C++ Classes with C Shims
4 min

C++ libraries are notoriously difficult for FFI systems to bind. Unlike C, which uses standard symbol names, C++ "mangles" function names (for example, turning Warrior::attack() into _ZN7Warrior6attackEv) to support features like overloading. Furthermore, C++ methods require a hidden this pointer to know which object instance they are acting upon.

To capture a C++ class, we must bridge the gap by creating a C shim. This is a thin layer of extern "C" functions that expose the C++ class creation, destruction, and methods as standard C functions. This is the cheap and easy route.

The Recipe

This time, we're creating a simple C++ Droid class, wrap it in a C-compatible API, and then build a Perl class to manage the object's lifecycle automatically.

use v5.40;
use Affix qw[:all];
use Affix::Build;
$|++;

# 1. The C++ Class and C Shim
# We include both the class definition and the extern "C" wrappers.
my $src = <<~'END_CPP';
    #include <iostream>

    /* Pure C++ Class */
    class Droid {
        int serial;
    public:
        Droid(int s) : serial(s) {}
        ~Droid() {
            std::cout << "Droid " << serial << " is being scrapped." << std::endl;
        }
        void speak() {
            std::cout << "Droid " << serial << " says: Roger roger." << std::endl;
        }
    };

    /* The C ABI Shim */
    extern "C" {
        // Constructor Wrapper
        void* Droid_new(int serial) {
            return new Droid(serial);
        }

        // Method Wrapper (Pass object pointer explicitly)
        void Droid_speak(void* d) {
            static_cast<Droid*>(d)->speak();
        }

        // Destructor Wrapper
        void Droid_delete(void* d) {
            delete static_cast<Droid*>(d);
        }
    }
    END_CPP

# 2. Compile
# Affix::Build detects C++ by file extension or flags.
# We'll treat this string as .cpp content.
my $c = Affix::Build->new;
$c->add( \$src, lang => 'cpp' );
my $lib = $c->link;

# 3. Define the Perl Class
package Droid {
    use Affix qw[:all];

    # Define the pointer type for clarity
    # We treat the C++ object as an opaque Void Pointer in Perl
    typedef DroidPtr => Pointer [Void];

    # Bind the Shim functions
    affix $lib, 'Droid_new',    [Int]          => DroidPtr();
    affix $lib, 'Droid_speak',  [ DroidPtr() ] => Void;
    affix $lib, 'Droid_delete', [ DroidPtr() ] => Void;

    sub new( $class, $serial ) {

        # Call C++ new, get the pointer
        my $ptr = Droid_new($serial);

        # Bless the pointer into a reference to make it a Perl object
        return bless \$ptr, $class;
    }

    sub speak($self) {

        # Dereference to get the raw pointer and pass to shim
        Droid_speak($$self);
    }

    sub DESTROY($self) {

        # Automatically clean up C++ memory when Perl object dies
        Droid_delete($$self);
    }
}

# 4. Run it
{
    say 'Fabricating unit...';
    my $r2 = Droid->new(101);
    say 'Commanding unit...';
    $r2->speak();
    say 'Unit going out of scope...';
}
say 'Done.';

Output:

Fabricating unit...
Commanding unit...
Droid 101 says: Roger roger.
Unit going out of scope...
Droid 101 is being scrapped.
Done.

How It Works

  • 1. The extern "C" Block

    This tells the C++ compiler: "Do not mangle these names." This ensures Droid_new appears in the library exactly as written, making it findable by Affix.

  • 2. The Opaque Pointer (void*)

    Perl does not need to know the layout of the class Droid. To Perl, the object is just a memory address. In the C shim, we accept void* and static_cast it back to Droid* to call methods.

  • 3. Lifecycle Management

    • Construction: Droid_new allocates memory on the C++ heap (new Droid).
    • Destruction: We bind Droid_delete to Perl's DESTROY method. When the Perl variable $r2 goes out of scope, Perl calls DESTROY, which calls delete in C++, preventing memory leaks.

Kitchen Reminders

  • Catching Exceptions

    You must catch all C++ exceptions within your C shim. If a C++ exception throws across the FFI boundary into Perl, your program will crash instantly. Use try { ... } catch (...) { ... } blocks inside your shim functions to handle errors gracefully (perhaps returning an error code).

  • Virtual Methods

    If you need to extend a C++ class in Perl (i.e., override a virtual method in Perl), the C shim must be more complex. You would do something like create a C++ child class that holds a Perl callback, forwarding the virtual call back to Perl.

Chapter 29: An Affix Module Factory
3 min

While runtime wrapping is convenient, it has a startup cost (parsing headers every time). For a distributable CPAN module, you want the parsing to happen once (on your machine), generating a pure Perl module that users can load instantly.

Affix::Wrap objects have an affix_type method that returns the Perl code required to bind that entity. We can use this to write a code generator.

The Recipe

This recipe will generate a file named MyLib.pm from a C header, complete with POD documentation extracted from C comments.

use v5.40;
use Affix::Wrap;
use Path::Tiny;

# 1. The Source Header
my $header = Path::Tiny->tempfile( SUFFIX => '.h' );
$header->spew_utf8(<<~'H');
    /**
     * @brief A user profile.
     */
    typedef struct {
        int id;
        char name[32];
    } User;

    /**
     * @brief Fetch a user from the database.
     * @param id The user ID to lookup
     * @return A User struct
     */
    User get_user(int id);
    H

# 2. Parse
my $binder = Affix::Wrap->new( project_files => ["$header"] );
my @nodes  = $binder->parse;

# 3. Generate Perl Code
my $code = <<~'PERL';
    package MyLib;
    use v5.40;
    use Affix;

    # In a real module, you might locate the lib via Alien::Base or File::ShareDir
    my $lib = Affix::load('mylib');
    PERL
for my $node (@nodes) {

    # Spew POD
    if ( my $doc = $node->pod ) {
        $code .= "\n$doc=cut\n\n";
    }

    # Generate Affix calls
    if ( $node isa Affix::Wrap::Function ) {
        $code .= $node->affix_type . "\n";
    }
    elsif ( $node isa Affix::Wrap::Typedef ) {
        $code .= $node->affix_type . ";\n";
    }
    else {
        ...;    # etc.
    }
}
$code .= "\n1;";

# 4. Save
path('MyLib.pm')->spew_utf8($code);
say $code;

Output (The generated file):

package MyLib;
use v5.40;
use Affix;

# In a real module, you might locate the lib via Alien::Base or File::ShareDir
my $lib = Affix::load('mylib');

=head2 User

A user profile.

=cut

typedef User => Struct[ id => Int, name => Array[Char, 32] ];

=head2 get_user

Fetch a user from the database.

=over

=item C<id>

The user ID to lookup

=back

B<Returns:> A User struct

=cut

affix $lib, get_user => [Int], User;

1;

How It Works

  • affix_type This method returns the string representation of the binding.

    • For User: Struct[ id => Int, name => Array[Char, 32] ].
    • For get_user: affix $lib, get_user => [Int], User;.
  • pod Affix::Wrap parses Doxygen-style comments (@brief, @param) and converts them into standard Perl POD, meaning your generated module is automatically documented!

Kitchen Reminders

  • Dependencies: The generated .pm file does not depend on Affix::Wrap or clang. It only depends on Affix. This means your end-users do not need a compiler installed to use your module.
Chapter 28: Instant Runtime Wrappers
3 min

The fastest way to use Affix::Wrap is Runtime Wrapping. In this workflow, you parse the headers and bind the functions immediately when your script starts. This is ideal for rapid prototyping, internal scripts, or testing, as you don't need to generate a separate .pm file.

The Recipe

In this demo, we'll use Affix::Build to build a library and Affix::Wrap to wrap it instantly.

use v5.40;
no warnings 'once';
use Affix qw[:all];
use Affix::Build;
use Affix::Wrap;
use Path::Tiny;

# 1. Setup the C Project
my $dir = Path::Tiny->tempdir( CLEANUP => 0 );
my $src = $dir->child('physics.c');
my $hdr = $dir->child('physics.h');

# A header with documentation
$hdr->spew_utf8(<<~'H');
    typedef struct { double x, y, z; } Vector3;

    /**
     * Adds two vectors.
     */
    Vector3 vec_add(Vector3 a, Vector3 b);

    // Global gravity constant
    extern double GRAVITY;
    H

# Implementation
$src->spew_utf8(<<~'C');
    #include "physics.h"
    #ifdef _WIN32
    __declspec(dllexport)
    #endif
    double GRAVITY = 9.81;

    #ifdef _WIN32
    __declspec(dllexport)
    #endif
    Vector3 vec_add(Vector3 a, Vector3 b) {
        Vector3 r = { a.x + b.x, a.y + b.y, a.z + b.z };
        return r;
    }
    C

# 2. Compile the library
my $compiler = Affix::Build->new( build_dir => $dir );
$compiler->add($src);
my $lib_path = $compiler->link;

# 3. The Magic: Bind it!
Affix::Wrap->new( project_files => [$hdr], include_dirs => [$dir] )->wrap($lib_path);

# 4. Use it
# The functions and typedefs are now installed in our package!
say "Gravity is: $main::GRAVITY";
my $v1 = { x => 1, y => 2, z => 3 };
my $v2 = { x => 4, y => 5, z => 6 };

# Affix::Wrap automatically handled the Struct definition for Vector3
my $v3 = vec_add( $v1, $v2 );
say "Result: $v3->{x}, $v3->{y}, $v3->{z}";

Output:

Gravity is: 9.81
Result: 5, 7, 9

How It Works

  • ->wrap($lib) This method iterates over every node found in the AST.
    1. Typedefs/Structs: It calls Affix::typedef, registering types like Vector3 so they can be used in signatures.
    2. Variables: It calls Affix::pin, tying the Perl variable $GRAVITY to the symbol in the shared library.
    3. Functions: It calls Affix::affix, creating the Perl subroutine vec_add.

Note that we didn't write a single Struct[...] or affix signature manually.

Kitchen Reminders

  • Namespaces: By default, wrap installs symbols into the caller's package. If you want to keep things clean, you can wrap into a dedicated class:

    package Physics {
        Affix::Wrap->new(...)->wrap($lib, 'Physics');
    }
    say Physics::vec_add(...);
    
Chapter 27: Automated Introspection with Affix::Wrap
3 min

Writing affix signatures manually is great for control, but it becomes tedious when wrapping a library with hundreds of functions and structs. Instead of staring at a .h file and manually translating C types to Affix types, let Affix::Wrap automate it!

Affix::Wrap is a friction-less introspection engine that parses C/C++ header files and builds a structured Abstract Syntax Tree (AST) of the library. It can detect functions, variables, macros, enums, and complex nested structs.

The Drivers

Affix::Wrap uses a dual-driver approach to read C code:

  1. Clang Driver (Preferred): If the clang executable is found in your path, Affix::Wrap uses it to compile the header and dump the AST in JSON format. This is extremely accurate. It handles preprocessor macros, include paths, and complex typedef chains exactly as a C compiler would.
  2. Regex Driver (Fallback): If Clang is missing, it falls back to a pure Perl, regex based parser. This is fast and has zero external dependencies but it may struggle with highly complex C++ templates or obscure preprocessor magic.

The Recipe

This time, let's parse a sample C header to see what Affix::Wrap sees.

use v5.40;
use Affix::Wrap;
use Path::Tiny;
use Data::Dump;

# 1. Create a dummy header file
my $header = Path::Tiny->tempfile( SUFFIX => '.h' );
$header->spew_utf8(<<~'C');
    /**
     * @brief A 2D Point
     */
    typedef struct {
        int x;
        int y;
    } Point;

    #define MAX_POINTS 100

    /**
     * @brief Calculate distance
     * @param p The point to measure
     */
    double distance(Point p);
    C

# 2. Parse the header
my $binder = Affix::Wrap->new( project_files => ["$header"] );
my @nodes  = $binder->parse;

# 3. Inspect the AST
foreach my $node (@nodes) {
    if ( $node isa Affix::Wrap::Function ) {
        say 'Found Function: ' . $node->name;
        say ' - Returns: ' . $node->ret->name;
    }
    elsif ( $node isa Affix::Wrap::Macro ) {
        say 'Found Macro: ' . $node->name . ' = ' . $node->value;
    }
    elsif ( $node isa Affix::Wrap::Typedef ) {
        say 'Found Typedef: ' . $node->name;
        say '  ' . $node->affix_type;
    }
}

Output:

Found Typedef: Point
  typedef Point => Struct[ x => Int, y => Int ]
Found Macro: MAX_POINTS = 100
Found Function: distance
 - Returns: double

How It Works

The parse method returns a list of Affix::Wrap::Entity objects. These objects understand the C types they represent. For example, the Struct object contains a list of Member objects, which in turn hold Type objects.

Affix::Wrap flattens complex relationships. Notice in the C code, Point was a typedef around an anonymous or named struct. Affix::Wrap detects this common pattern and merges them, presenting you with a single named Struct entity.

Kitchen Reminders

  • Includes: If your header includes other files (like <stdio.h> or "my_types.h"), pass their locations to the constructor via include_dirs => ['/path/to/includes'].
  • Documentation: Affix::Wrap also captures comments! In the next chapters, we will use this to generate documentation automatically.
Chapter 26: The "Kitchen Sink" Polyglot Library
4 min

Sometimes, the best tool for the job involves three different tools. You might have legacy math models in Fortran, performance-critical loops in Assembly, and a clean C API to orchestrate them.

By mastering the Polyglot strategy, you're no longer forced to rewrite battle-tested legacy code or compromise on performance. You can let Fortran handle the numerics, Assembly handle the vectorization, and C handle do coordination, all while driving the whole machine from Perl. Affix::Build turns what used to be a nightmare of Makefiles and linker flags into a simple script, freeing you to choose the absolute best tool for every specific problem.

The Recipe

Here, we'll create a library called number_cruncher. It uses C as the API controller, Fortran for the calculation logic, and Assembly for a low-level incrementer. It's not a very useful example but it's a good demonstration of our task.

use v5.40;
use Affix qw[:all];
use Affix::Build;
use Config;

# 1. Initialize
# We assume we are compiling different languages into one binary
my $c = Affix::Build->new( name => 'number_cruncher' );

# 2. C Source (The Orchestrator)
# Standard C function that will be the entry point or helper
$c->add( \<<~'', lang => 'c' );
    #include <stdio.h>
    #ifdef _WIN32
    __declspec(dllexport)
    #endif
    int core_version() { return 1; }

# 3. Fortran Source (The Math Engine)
# Uses ISO C Binding to export 'fortran_add' as a standard C symbol
$c->add( \<<~'', lang => 'f90' );
    function fortran_add(a, b) bind(c, name='fortran_add')
        use iso_c_binding
        integer(c_int), value :: a, b
        integer(c_int) :: fortran_add
        fortran_add = a + b
    end function

# 4. Assembly Source (The Optimizer)
# A simple function 'asm_inc(x)' that returns x + 1.
# We must detect the CPU architecture to provide the correct instructions.
my $arm = $Config{archname} =~ /arm64|aarch64/;
$c->add( \( $arm ? <<~'' : $^O eq 'MSWin32' ? <<~'': <<~'' ), lang => $arm ? 's' : 'asm' );
        ; ARM64: Linux, macOS, Windows ARM uses standard system assembler (.s)
        .global asm_inc
        .text
        .align 2
        asm_inc:
            add w0, w0, #1  ; Increment w0 (first arg) by 1
            ret

        ; Win64 x86_64: Uses RCX for first argument
        global asm_inc
        section .text
        asm_inc:
            mov eax, ecx
            inc eax
            ret

        ; SysV x86_64: Uses RDI for first argument
        global asm_inc
        section .text
        asm_inc:
            mov eax, edi
            inc eax
            ret


# 5. Build
# The compiler detects mixed languages and switches to 'Polyglot Mode'
my $lib = $c->link;
say 'Compiled Polyglot Library: ' . $lib;

# 6. Bind
affix $lib, 'core_version', []           => Int;
affix $lib, 'fortran_add',  [ Int, Int ] => Int;
affix $lib, 'asm_inc',      [Int]        => Int;

# 7. Run
say 'Core Version: ' . core_version();
say 'Fortran Add:  ' . fortran_add( 10, 20 );
say 'ASM Inc:      ' . asm_inc(99);

Output:

Compiled Polyglot Library: C:/Users/S/AppData/Local/Temp/Fye21DPXMw/number_cruncher.dll.1
Core Version: 1
Fortran Add:  30
ASM Inc:      100

How It Works

  • The Polyglot Strategy

    When Affix::Build detects multiple languages, it switches from 'Native' mode (one compiler does everything) to 'Polyglot' mode.

    1. It asks gcc to compile the C code to math_core.o.
    2. It asks gfortran to compile the Fortran code to math_algos.o.
    3. It asks nasm (or the system assembler on ARM) to assemble the ASM code to fast.o.
    4. It invokes the system linker (usually via cc or c++) to combine all three objects into the final shared library.

Kitchen Reminders

  • ABI Compatibility

    The reason this works is that C, Fortran, and Assembly all speak the same "C ABI" (Application Binary Interface). Rust, Zig, and Odin also support this. Languages like Go and C# require heavier runtimes, but Affix::Build handles the initialization flags for you.

  • Name Mangling

    If you mix C++ into this stack, remember to wrap your exported functions in extern "C". Without it, the C++ compiler will mangle the names (like _Z8func_cppv or whatever your compiler's ABI defines), and the linker won't be able to match them if another language tries to call them. We'll demonstrate how you'd work around this in a future chapter.

Chapter 25: The Instant Assembly/C#/D/Fortran/Go/Rust/Zig Library
6 min

C might be the lingua franca of system programming, but Affix::Build is a true polyglot. We've covered building shared libraries in C and C++ but Affix::Build supports over 15 languages out of the box.

This chapter is a quick-reference gallery. Each recipe demonstrates how to compile a single function that adds two integers, exported in a way that Affix can easily bind to it.

Assembly

For raw speed, you can write directly in Assembly.

Affix::Build supports both x86_64 (via NASM) and ARM64 (via the system assembler). Calling conventions differ by architecture and operating system, so we check %Config to determine which assembly code to use.

use v5.40;
use Affix qw[:all];
use Affix::Build;
use Config qw[%Config];
$|++;
#
my $c   = Affix::Build->new( name => 'asm_lib' );
my $arm = $Config{archname} =~ /arm64|aarch64/;
$c->add( \( $arm ? <<~'' : $^O eq 'MSWin32' ? <<~'': <<~'' ), lang => $arm ? 's' : 'asm' );
        ; ARM64: add w0, w0, w1
        .global add
        .text
        .align 2
        add:
            add w0, w0, w1
            ret

        ; Win64 x86_64: RCX + RDX -> RAX
        global add
        section .text
        add:
            mov eax, ecx
            add eax, edx
            ret

        ; SysV x86_64: RDI + RSI -> RAX
        global add
        section .text
        add:
            mov eax, edi
            add eax, esi
            ret

#
affix $c->link, 'add', [ Int, Int ] => Int;
say add( 4, 5 );

C# (.NET 8+)

With .NET 8's NativeAOT feature, you can compile C# directly to native machine code (no .NET runtime required on the target machine).

use v5.40;
use Affix qw[:all];
use Affix::Build;
$|++;
#
my $c = Affix::Build->new( name => 'cs_lib' );
$c->add( \<<~'CS', lang => 'cs' );
    using System.Runtime.InteropServices;
    namespace Demo {
        public class Math {
            [UnmanagedCallersOnly(EntryPoint="add")]
            public static int Add(int a, int b) => a + b;
        }
    }
    CS
#
affix $c->link, 'add', [ Int, Int ] => Int;
say add( 4, 5 );

D (Dlang)

D is unique: on Windows, a specific initialization mixin is required to ensure its runtime starts correctly when loaded as a DLL.

use v5.40;
use Affix qw[:all];
use Affix::Build;
$|++;
#
my $c   = Affix::Build->new( name => 'd_lib' );
my $src = ( $^O eq 'MSWin32' ? <<~'WIN32' : '' ) . <<~'D';
    import core.sys.windows.dll;
    mixin SimpleDllMain;
    export
WIN32
    extern(C) int add(int a, int b) { return a + b; }
D
$c->add( \$src, lang => 'd' );
#
affix $c->link, 'add', [ Int, Int ] => Int;
say add( 4, 5 );

Fortran

Modern Fortran (2003+) offers the iso_c_binding module, which ensures your functions use standard C types and calling conventions, avoiding the historic "name mangling" issues (like appended underscores).

use v5.40;
use Affix qw[:all];
use Affix::Build;
$|++;
#
my $c = Affix::Build->new( name => 'fortran_lib' );
$c->add( \<<~'F90', lang => 'f90' );
    function add(a, b) bind(c, name='add')
        use iso_c_binding
        integer(c_int), value :: a, b
        integer(c_int) :: add
        add = a + b
    end function
F90
#
affix $c->link, 'add', [ Int, Int ] => Int;
say add( 4, 5 );

Go (Golang)

Go functions must be marked with the special //export comment to be visible.

Note: The Go runtime spins up background threads (for GC and scheduling) that do not shut down cleanly when a shared library is unloaded. This often causes access violations on Windows during program exit.

Affix attempts to detect Go libraries (by looking for the _cgo_dummy_export symbol) and pin them in memory to prevent this crash. However, if you still encounter segmentation faults during global destruction, you can force a safe exit using POSIX::_exit(0).

use v5.40;
use Affix qw[:all];
use Affix::Build;
$|++;
#
my $c = Affix::Build->new( name => 'go_lib' );
$c->add( \<<~'GO', lang => 'go' );
    package main
    import "C"

    //export add
    func add(a, b C.int) C.int {
        return a + b
    }

    func main() { }
    GO
#
affix $c->link, 'add', [ Int, Int ] => Int;
say add( 4, 5 );

Rust

Rust is an excellent candidate for FFI because it has no garbage collector and strict ABI control. You must use #[no_mangle] and extern "C" to prevent the compiler from changing your function names.

use v5.40;
use Affix qw[:all];
use Affix::Build;
#
my $c = Affix::Build->new( name => 'rust_lib' );
$c->add( \<<~'RUST', lang => 'rust' );
    #[no_mangle]
    pub extern "C" fn add(a: i32, b: i32) -> i32 {
        a + b
    }
RUST
#
my $lib = $c->link;
affix $lib, 'add', [ Int, Int ] => Int;
say add( 4, 5 );

Zig

Zig is fully C-ABI compatible. Use the export keyword to make functions available.

use v5.40;
use Affix qw[:all];
use Affix::Build;
$|++;
#
my $c = Affix::Build->new( name => 'zig_lib' );
$c->add( \<<~'ZIG', lang => 'zig' );
    export fn add(a: i32, b: i32) i32 {
        return a + b;
    }
ZIG
#
affix $c->link, 'add', [ Int, Int ] => Int;
say add( 4, 5 );

Chef's Notes

This gallery is just a sample but demonstrates that Perl's role as the ultimate glue language is stronger than ever. By abstracting away the build chain complexities... from the strict safety of Rust to the raw speed of Assembly, Affix::Build allows you to reach far outside the CPAN ecosystem. You can now leverage the unique strengths of almost any language, modern or ancient, without ever leaving the comfort of Perl.

In our next chapter, we'll discuss merging some of these languages into a single shared library.

Chapter 24: The Instant C++ Library
4 min

Throughout this cookbook, you've seen me use Affix::Build to generate shared libraries on the fly from C. However, building shared libraries isn't a concept limited to just C so neither are the capabilities of Affix::Build.

Affix::Build, first introduced in our very first chapter, is a cross-platform, multi-lingual compilation wrapper. While it supports over 15 languages (including Rust, Go, Fortran, and D), it is particularly useful for C and C++ because it abstracts away the differences between GCC, Clang, and MSVC, handles operating system extensions (.dll, .so, .dylib), automatically locates the correct driver to link the standard libraries properly, and applies critical flags like -fPIC where required.

In this chapter, we'll demonstrate that as we build a library that uses the C++ Standard Library (STL) to reverse a string in place.

The Recipe

use v5.40;
use Affix qw[:all];
use Affix::Build;

# 1. Configure the Compiler
# We enable debug mode to see the underlying compiler commands
my $compiler = Affix::Build->new( name => 'str_util', debug => 1 );

# 2. Add C++ Source
# We pass a reference to a string containing the source code.
# We must specify 'lang => cpp' so the compiler knows to use a C++ driver.
$compiler->add( \<<~'CPP', lang => 'cpp' );
    #include <algorithm>
    #include <string>
    #include <cstring>

    extern "C" {
        // We use C linkage so Affix can find the symbol easily
        void reverse_it(char* buffer) {
            if (!buffer) return;

            // Use high-level C++ features
            std::string s(buffer);
            std::reverse(s.begin(), s.end());

            // Copy back to the raw buffer
            std::strcpy(buffer, s.c_str());
        }
    }
    CPP

# 3. Compile and Link
# This generates the shared object in a temp folder and returns the path.
my $lib_path = $compiler->link;
say 'Library compiled to: ' . $lib_path;

# 4. Bind and Call
affix $lib_path, 'reverse_it', [String] => Void;
my $text = 'Affix makes C++ easy';
reverse_it($text);
say $text;

Output:

[Affix] Exec: g++ -shared C:/Users/S/AppData/Local/Temp/Yx9npzzak9/source_001.cpp -o C:/Users/S/AppData/Local/Temp/Yx9npzzak9/str_util.dll.1
Library compiled to: C:/Users/S/AppData/Local/Temp/Yx9npzzak9/str_util.dll.1
ysae ++C sekam xiffA

How It Works

  • 1. Language Detection

    When you call $compiler->add( ..., lang => 'cpp' ), the compiler updates its internal linking strategy. It knows that standard C compilers (gcc) cannot link C++ standard libraries (libstdc++) by default. It resolves the correct driver on your system (preferring g++ or clang++) to ensure the STL is available.

  • 2. Inline Source

    Passing a SCALAR reference (\<<~'CPP') tells the compiler to write that content to a temporary file before compiling. You can also pass a standard file path (e.g., $compiler->add('src/utils.cpp')), in which case the lang parameter is optional (auto-detected by extension).

  • 3. Platform Abstraction

    The call to $compiler->link performs a lot of heavy lifting:

    • Windows: It knows to use the .dll extension and creates a PE binary.
    • Linux/Unix: It uses .so or .dylib, adds the -fPIC (Position Independent Code) flag required for shared libraries, and ensures the library name is prefixed with lib if required by the OS loader.

Bonus: The Polyglot Strategy

Affix::Build can mix languages in a single library. If you add files from different languages (e.g., C and Rust), the compiler switches strategies:

  1. It compiles every source file into a static object file (.o or .a) using the appropriate compiler for each language (rustc for Rust, cc for C, etc.).
  2. It invokes the system linker to combine all static objects into one shared library.
# Example: Using a Rust function inside a C wrapper
$compiler->add( 'src/calc.rs' );  # Contains #[no_mangle] pub extern "C" fn add...
$compiler->add( 'src/main.c' );   # Calls 'add' from Rust
$compiler->link;                  # Links them into one DLL

We'll revisit this concept in future chapters.

Kitchen Reminders

  • Compiler Flags

    You can optimize your build or include external headers using the flags parameter in the constructor.

    my $c = Affix::Build->new(
        flags => {
            cxxflags => '-O3 -I/usr/local/include/my_lib',
            ldflags  => '-L/usr/local/lib -lmy_lib'
        }
    );
    
  • Windows MinGW

    If you are using Strawberry Perl, Affix::Build automatically handles the whole-archive linking issues often encountered when bundling static C++ libraries into a DLL, ensuring no symbols are stripped during the link phase.

Chapter 23: Battle of the Bindings
3 min

"Is Affix faster than... everything else?"

Let's find out!

The Perl ecosystem offers several ways to call C code. How does Affix stack up?

We will benchmark four approaches:

  1. Inline::C

    Compiles C code into a custom XS module. This is historically the fastest way to call C from Perl, as it eliminates the "FFI" layer entirely, but it requires a C compiler at installation time (or runtime).

  2. FFI::Platypus

    The current dominant FFI module on CPAN. It uses libffi (a portable, general-purpose Foreign Function Interface library) to handle argument shuffling.

  3. Affix

    Our pride and joy. Wraps our custom JIT engine: infix.

The Recipe

We'll compile a shared library performing a simple integer addition, then bind it using all three libraries.

use v5.40;
use Benchmark qw[cmpthese];
use Affix     qw[:all];
use Affix::Build;
use FFI::Platypus 2.00;

# All tests run on the same C.
my $c_code;
BEGIN { $c_code = <<~'' }
    int add_ints(int a, int b) {
        return a + b;
    }


# Inline::C setup
use Inline C => Config => ( ccflags => '-O3' );
use Inline C => $c_code;

# We must compile the library for Affix and Platypus
my $c = Affix::Build->new( flags => { cflags => '-O3' } );
$c->add( \$c_code, lang => 'c' );
my $lib = $c->link;

# FFI::Platypus setup
my $ffi_plat = FFI::Platypus->new( api => 2 );
$ffi_plat->lib($lib);
$ffi_plat->attach( [ add_ints => 'add_platypus' ], [ 'int', 'int' ], 'int' );

# Affix setup
affix $lib, [ add_ints => 'add_affix' ], [ Int, Int ] => Int;

# The race
say 'Benchmarking for 5 seconds...';
my $x = 100;
my $y = 200;
cmpthese - 5, {
    Inline   => sub { add_ints( $x, $y ) },
    Platypus => sub { add_platypus( $x, $y ) },
    Affix    => sub { add_affix( $x, $y ) }
    }

The Results

On my system, I see results similar to this:

Benchmarking for 5 seconds...
               Rate Platypus   Inline    Affix
Platypus  5026460/s       --     -57%     -82%
Inline   11641793/s     132%       --     -58%
Affix    28016636/s     457%     141%       --

Analysis

  • Affix vs. XS (Inline::C)

    To the reader, the most startling result is that Affix is over 120% faster than Inline::C. Traditionally, XS (which Inline::C generates) was considered the speed limit for Perl. Affix beats this limit by generating a custom JIT trampoline that reads Perl's internal SVs directly, skipping the overhead of the Perl stack macros (dXSARGS, ST(n)) that XS relies on. It is effectively "Inline Assembly" for Perl.

  • Affix vs. Platypus

    Affix runs over 4x faster than Platypus in this test. While Platypus relies on libffi (a stable, generic library), libffi must handle platform logic at runtime. Affix generates specific machine code for the exact call signature, allowing for drastic optimizations.

Summary

  • Use Affix for flexibility, safety, and complex types (Structs, Arrays). As the benchmarks show, you sacrifice absolutely nothing in speed; in fact, you'll likely gain performance over hand-written XS.
  • Use Inline::C if you are already comfortable writing XS macros and want static compilation, though you may actually lose performance compared to Affix unless you really focus on optimizing your own hot path.
  • Use FFI::Platypus if you already already have code written and don't feel like moving to Affix?
Chapter 22: Profiling Proves Peak Performance
3 min

"Is Affix faster than pure Perl?"

This is the most important question. Migrating code to C requires an investment of time (and maybe the development of a new skillset) so the payoff must be worth it.

The simple answer is usually, "Yes, but..."

You must understand that there is a fixed cost to crossing the boundary between Perl and C (marshalling arguments, setting up the stack, the JIT trampoline, etc.). If your C function does very little (like adding two integers), the overhead might outweigh the speed gain. However, if your C function does heavy lifting (matrix math, cryptography, parsing), the gain is massive.

Let's benchmark a naive Fibonacci calculation in Perl versus C to see exactly how much faster native code can be.

The Recipe

use v5.40;
use Affix qw[:all];
use Affix::Build;
use Benchmark qw[cmpthese];
$|++;

# 1. Compiled C Implementation
# We use -O3 to ensure the C compiler optimizes the recursion as much as possible
my $c = Affix::Build->new( flags => { cflags => '-O3' } );
$c->add( \<<~'', lang => 'c' );
    int fib_c(int n) {
        if (n < 2) return n;
        return fib_c(n-1) + fib_c(n-2);
    }

affix $c->link, 'fib_c', [Int] => Int;

# 2. Perl Implementation
sub fib_p($n) {
    return $n if $n < 2;
    return fib_p( $n - 1 ) + fib_p( $n - 2 );
}

# 3. Benchmark
say 'Small N (High overhead impact)';
cmpthese(
    -5,
    {   Perl  => sub { fib_p(10) },
        Affix => sub { fib_c(10) }
    }
);

say 'Large N (Raw compute dominance)';
cmpthese(
    -5,
    {   Perl  => sub { fib_p(30) },
        Affix => sub { fib_c(30) }
    }
);

These are the results from my machine:

Small N (High overhead impact)
            Rate   Perl  Affix
Perl     47892/s     --  -100%
Affix 10607023/s 22048%     --
Large N (Raw compute dominance)
        Rate   Perl  Affix
Perl  3.16/s     --  -100%
Affix  959/s 30258%     --

How It Works

  • 1. The Boundary Cost

    In the "Small N" test, Affix leans on C to overcome the overhead of marshalling data back and forth. Even with the FFI 'tax,' the compiled C code processes the recursion so efficiently that it still runs 200x faster than the Perl subroutine.

  • 2. The Compute Gain

    In the "Large N" test, the difference widens. The C code runs entirely in native machine code for millions of recursive iterations before crossing back to Perl. As you can see, this results in the C version being over 300x faster than pure Perl.

Kitchen Reminders

  • Batching

    If you have a C function that sets a single pixel, calling it 2 million times from Perl will be slow. Instead, expose a C function that takes an Array[Pixel] and updates the whole image in one call. We'll get to even more advanced features like SIMD in later chapters.

Chapter 21: Perl Callbacks in C Structs (vtables)
3 min

In C, OOP is often simulated using structs containing function pointers. This pattern is commonly known as a vtable and allows a library to call different implementations of a function depending on the object it's holding. With Affix, you can populate these fields with Perl subroutines, effectively creating a Perl class that C can call into.

We're going to use a vtable to build a hypothetical plugin system. The C library expects a struct containing handlers for init and process.

The Recipe

use v5.40;
use Affix qw[:all];
use Affix::Build;

# 1. Compile C Library
my $c = Affix::Build->new();
$c->add( \<<~'END', lang => 'c' );
    typedef struct {
        const char* name;
        int (*init)(void);
        void (*process)(const char* data);
    } Plugin;

    void run_plugin(Plugin* p, const char* data) {
        if (p->init()) {
            p->process(data);
        }
    }
    END
my $lib = $c->link;

# 2. Define Types
# The struct fields must include the Callback signature.
typedef Plugin => Struct [ name => String, init => Callback [ [] => Int ], process => Callback [ [String] => Void ] ];

# 3. Bind
affix $lib, 'run_plugin', [ Pointer [ Plugin() ], String ] => Void;

# 4. Create the Perl "Object"
# We use a HashRef to represent the struct.
# The callback fields are populated with anonymous subs.
my $my_plugin = {
    name => 'PerlPlugin v1.0',
    init => sub {
        say 'Initializing Perl Plugin...';
        return 1;    # Success
    },
    process => sub ($data) {
        say 'Processing data in Perl: ' . $data;
    }
};

# 5. Call C
# We pass the reference to our hash. Affix packs it into the C struct,
# creating trampolines for the subroutines automatically.
run_plugin( $my_plugin, 'Some Input Data' );

How It Works

  • 1. Nested callbacks

    init => Callback[ [] => Int ]
    

    When you define a struct member as a Callback, Affix expects a Perl code reference in that hash field.

  • 2. Automatic trampolining

    When you pass the HashRef $my_plugin to the C function, Affix:

    1. Allocates the C struct.
    2. Copies the string 'PerlPlugin v1.0'.
    3. Asks infix to generate reverse trampolines for the init and process subs.
    4. Stores the function pointers to those trampolines in the struct and passes the whole thing on to our library.
  • 3. Execution

    When C calls p->process(data), it hits the trampoline which bounces everything over to your Perl sub.

Kitchen Reminders

  • Lifetime management

    The trampolines generated for struct members exist only for the duration of the call to run_plugin. If the C library stores this struct pointer globally and tries to call the callbacks after run_plugin returns, the program will crash.

    If you need persistent callbacks, you must manually allocate the struct using calloc and assign the fields, ensuring the Perl variables stay in scope.

Chapter 20: Automatic Resource Management (RAII)
1 min

Manual free() is error-prone. We can use Perl's DESTROY phase to automate it.

The Recipe

use v5.40;
no warnings 'experimental';
use feature 'class';
use Affix qw[:all];

class AutoPointer {
    field $ptr : reader : param;

    method DESTROY {
        Affix::free($ptr);    # Call C/malloc free()
    }
}

# Usage
{
    my $safe = AutoPointer->new( ptr => malloc(1024) );

    # ... use $safe->ptr ...
    Affix::dump( $safe->ptr, 1024 );
}    # $safe goes out of scope -> DESTROY called -> free() called.

How It Works

When the last reference to $safe disappears, Perl calls DESTROY. By wrapping our raw C pointer in a blessed object, we tie the C memory lifecycle to the Perl variable's scope. This is known as RAII (Resource Acquisition Is Initialization).

Chapter 19: Troubleshooting & Debugging
3 min

When things go wrong in Perl, you probably get an error message. When they go wrong in C, you get a segfault, a frozen terminal, or data corruption that only shows up three hours later. This chapter provides a survival kit for when things go very wrong and it's your job to figure it all out.

The Bug: The Vanishing Callback

A common source of crashes in FFI is the scope mismatch. Perl relies on reference counting while C expects you to know what you are doing.

# 1. C library (A Fake Event System)
# void register_handler(void (*cb)(void));
# void trigger_event();

# 2. Perl binding
affix $lib, 'register_handler', [ Callback[ [] => Void ] ] => Void;
affix $lib, 'trigger_event',    [] => Void;

# 3. The Buggy Code
sub setup {
    # We pass an anonymous subroutine.
    # Affix creates a trampoline for it.
    register_handler( sub { say "Event!" } );

    # END OF SCOPE
    # The anonymous sub may have a refcount of 0. If so, perl frees it and the reverse trampoline is destroyed.
}

setup();

# ... later ...
trigger_event(); # SEGFAULT! C jumps to freed memory.

The Fix

You must ensure the Perl CodeRef lives as long as the C library needs it.

my $KEEP_ALIVE; # Global or Object-level storage

sub setup {
    $KEEP_ALIVE = sub { say "Event!" };
    register_handler( $KEEP_ALIVE );
}

# ...
trigger_event(); # Works!

The Debugging Toolbox

Affix provides tools to inspect raw memory and internal states.

Affix::dump( $ptr, $bytes )

If you suspect struct alignment issues or garbage data, look at the raw bytes.

my $ptr = malloc(16);
# ... C writes to ptr ...

# Dump 16 bytes to STDOUT in Hex/ASCII format
Affix::dump($ptr, 16);

address( $ptr ) & is_null( $ptr )

Sanity check your pointers. A pointer value of 0 (NULL) or 0xFFFFFF... (Uninitialized memory) is a smoking gun.

my $ptr = some_c_function();
say sprintf("Pointer address: 0x%X", address($ptr));

errno()

If a C function returns -1 or NULL to indicate failure, it usually sets a system error code (errno/GetLastError).

if ( some_call() < 0 ) {
    die "C Error: " . errno();
}

4. sv_dump( $scalar )

Debugging the Perl side with this function that's a part of perl's internal API. If Affix refuses to accept a variable, checking its internal flags (Integer vs String, Readonly status) often reveals why.

my $val = 10;
Affix::sv_dump($val);
SV = IV(0x25b7a6b4118) at 0x25b7a6b4128
  REFCNT = 1
  FLAGS = (IOK,pIOK)
  IV = 10

Common Pitfalls

  • Writing to Strings

    String arguments (const char*) are read-only copies. If the C function modifies the string in-place, you MUST use Pointer[Char] and pass a mutable scalar.

  • Struct padding

    C compilers insert padding bytes to align members. If your Perl Struct[...] definition doesn't match the C compiler's packing rules, fields will be read from the wrong offsets. Use dump() to verify alignment.

  • VarArg types

    Passing a Perl float to a variadic function? It promotes to double. Passing an integer? It promotes to int64_t. If the C function expects a 32-bit int, use coerce(Int32, $val).

Chapter 18: Error Handling
1 min

System calls fail. In C, you check the global errno variable (or GetLastError() on Windows). In Affix, we expose this via errno().

The Recipe

We will try to open a file that doesn't exist.

use v5.40;
use Affix qw[:all];

# fopen returns NULL on failure
affix libc, 'fopen', [ String, String ] => Pointer [Void];
my $file = fopen( 'non_existent_file.txt', 'r' );
if ( is_null($file) ) {

    # get_system_error returns the numeric error code
    # In numerical context: errno
    # In string context:    strerror(errno)
    my $err = Affix::errno();
    die sprintf 'Could not open file: %s (%d)', $err, $err;
}

Kitchen Reminders

  • errno returns a dualvar

    • Numeric: 2 (ENOENT)
    • Stringified: "No such file or directory"

    This makes logging errors rather convenient.

Chapter 17: Advanced File Handling
6 min

Bridging I/O between languages is historically one of the most fragile aspects of FFI. While handling file descriptors manually as opaque pointers may be effective on POSIX systems, that approach is a minefield on Windows. Affix solves this by introducing two smart types: File and PerlIO. These types handle the extraction, translation, and lifecycle management of file handles automatically.

This chapter covers the correct usage patterns and advanced scenarios like multiplexing I/O between Perl and C on the same handle.

The Basic Recipe: Writing from C

Let's start with the basics: passing a Perl filehandle to a C function that expects FILE*.

Note the use of Pointer[File] in the signature; using just File would attempt to pass the opaque structure by value, which is not what we want.

use v5.40;
use Affix;
use Affix::Build;
#
# 1. Build the lib
my $c = Affix::Build->new();
$c->add( \<<~'', lang => 'c' );
    #include <stdio.h>
    void write_recipe(FILE* stream, const char* title) {
        if (stream) {
            fprintf(stream, "RECIPE: %s\n", title);
            fprintf(stream, "1. Preheat oven.\n");
            fprintf(stream, "2. Mix ingredients.\n");
            fflush(stream); // Important: C buffers IO. Flush if mixing with Perl IO.
        }
    }

my $lib = $c->link;

# 2. Bind the function
# CRITICAL STEP: Check your signature!
# Wrong:  [ File, String ]          <- Passes the whole skillet (Struct Copy)
# Right:  [ Pointer[File], String ] <- Passes the handle (Pointer)
affix $lib, 'write_recipe', [ Pointer [File], String ] => Void;

# 3. Start Cooking
open my $fh, '>', 'dinner_plans.txt' or die "Oven broke: $!";

# Affix unwraps the Perl glob, extracts the FILE*, and hands it to C.
write_recipe( $fh, 'Spicy Meatballs' );

# You may use normal file I/O functions
close $fh;

# 4. Verify
print $^O eq 'MSWin32' ? `type dinner_plans.txt` : `cat dinner_plans.txt`;

Advanced: Multiplexing I/O (Perl & C Interleaved)

A common point of failure in FFI is buffering. Perl has its own IO buffers, and C stdio has its own. If you mix print (Perl) and fprintf (C) on the same handle, the output often appears out of order.

To handle this, we rely on flushing. In this example, we mix Perl's unbuffered syswrite with C's standard I/O.

use v5.40;
use Affix;
use Affix::Build;

# 1. Add a C function that writes a specific block
my $c = Affix::Build->new();
$c->add( \<<~'', lang => 'c' );
    #include <stdio.h>
    void c_append_middle(FILE* f) {
        if (!f) return;
        fprintf(f, "[C] ...Middle Content...\n");
        fflush(f); // FLUSH is mandatory to sync with Perl's layer
    }

my $lib = $c->link;
affix $lib, 'c_append_middle', [ Pointer [File] ] => Void;
open my $fh, '>', 'multiplex.log' or die $!;

# 1. Perl writes the Header
syswrite $fh, "[Perl] Header\n";

# 2. C writes the Body
# We pass the same handle. Affix extracts the FILE* from it.
c_append_middle($fh);

# 3. Perl writes the Footer
syswrite $fh, "[Perl] Footer\n";
close $fh;

# Verify order is preserved
say 'Multiplex Results:';
print do { local ( @ARGV, $/ ) = 'multiplex.log'; <> };

Advanced: Reading from C

You can also open a file in Perl (handling errors and paths in high-level code) and hand it off to C for parsing or reading.

use v5.40;
use Affix;
use Affix::Build;
my $c = Affix::Build->new();
$c->add( \<<~'', lang => 'c' );
    #include <stdio.h>
    // Reads a single char from the stream and returns it as an Int
    int c_get_char(FILE* f) {
        if (!f) return -1;
        return fgetc(f);
    }

my $lib = $c->link;
affix $lib, 'c_get_char', [ Pointer [File] ] => Int;

# Create a dummy file
open my $w, '>', 'input.txt';
print $w 'ABC';
close $w;

# Open for reading in Perl
open my $reader, '<', 'input.txt';

# Read byte 1 via C
say 'Read from C: ' . chr c_get_char($reader);    # Output: A

# Read byte 2 via Perl
read $reader, my $buf, 1;
say 'Read from Perl: ' . $buf;                    # Output: B

# Read byte 3 via C
say 'Read from C: ' . chr c_get_char($reader);    # Output: C
close $reader;

The PerlIO Interface

If you are interfacing with XS modules or Perl internals (someday...), the API might expect PerlIO* instead of standard C FILE*. Affix provides the Pointer[PerlIO] type for this.

While Pointer[PerlIO] behaves similarly to Pointer[File] in usage (it unwraps the handle), it creates a distinct type check in the signature to prevent mixing up abstract PerlIO streams with raw stdio streams.

use v5.40;
use Affix;
use Affix::Build;
use Fcntl qw[SEEK_CUR SEEK_SET];
my $c = Affix::Build->new();
$c->add( \<<~'', lang => 'c' );
    // A function that theoretically accepts a PerlIO stream.
    // Since we aren't linking libperl in this stub, we use void*
    // to simulate an opaque handle, but in real XS, this would be PerlIO*.
    void* identity(void* stream) {
        return stream;
    }

my $lib = $c->link;

# We define the signature using Pointer[PerlIO] to enforce intent
affix $lib, 'identity', [ Pointer [PerlIO] ] => Pointer [PerlIO];
open my $fh, '+<', 'input.txt';
syswrite $fh, "Test\n";
sysseek $fh, 0, SEEK_SET;    # both handles will be at this position

# Round-trip the handle through C
my $returned_fh = identity($fh);

# Affix wraps the returned pointer in a new GLOB reference
say 'Original Handle: ' . $fh;
say 'Returned Handle: ' . $returned_fh;
say 'Original Handle Pos: ' . sysseek $fh,          0, SEEK_CUR;
say 'Returned Handle Pos: ' . sysseek $returned_fh, 0, SEEK_CUR;
say 'Reading returned handle: ' . <$returned_fh>;    # Could just as easily read the original
say 'Original Handle Pos: ' . sysseek $fh,          0, SEEK_CUR;
say 'Returned Handle Pos: ' . sysseek $returned_fh, 0, SEEK_CUR;

Kitchen Reminders

  • Key takeaway: Multiplexing file IO will likely require you pay close attention to buffering. Use fflush(f) or a similar function from C and syswrite or other unbuffered IO functions in Perl.

  • Summary of Types

    Perl Type C Equivalent Usage
    Pointer[File] FILE* Standard C I/O (stdio.h)
    Pointer[PerlIO] PerlIO* Perl Internal I/O or XS Extensions
    File struct FILE Opaque struct (Rarely used directly)
Chapter 16: Direct Marshalling Trampolines
3 min

Speed is the reason I wrote infix and Affix and I think I've achieved a good balance of features vs. speed. I've done a lot of work to reduce overhead on the hot path but what if we could go... faster?

Standard Affix calls involve a "marshalling" phase: Perl values are converted to C values, stored in a buffer, passed to the JIT trampoline, and then passed to the C function. This is flexible, but adds overhead. Direct marshalling removes the middleman; Affix generates a specialized JIT trampoline that knows how to read your Perl variables directly from SV*.

The Recipe

We will compare the standard wrap against direct_wrap using a simple addition function.

use v5.40;
use Affix qw[:all];
use Affix::Build;
use Benchmark qw[cmpthese];

# 1. Compile
my $c = Affix::Build->new();
$c->add( \<<~'END', lang => 'c' );
    int add(int a, int b) { return a + b; }
END
my $lib = $c->link;

# 2. Standard Bind
my $std_add = wrap $lib, 'add', [ Int, Int ] => Int;

# 3. Direct Bind
# The syntax is identical, just a different function name.
my $fast_add = direct_wrap $lib, 'add', [ Int, Int ] => Int;

# 4. Benchmark
cmpthese(
    -5,
    {   Standard => sub { $std_add->( 10, 20 ) },
        Direct   => sub { $fast_add->( 10, 20 ) }
    }
);

The Results

On my machine, direct marshalling can be faster for simple primitives.

               Rate Standard   Direct
Standard 21102786/s       --      -9%
Direct   23192942/s      10%       --

The difference between the two grows as the number of parameters grows. With a C function with just one more int param (int add(int a, int b, int c) { return a + b + c; }), the benchmarks look like this:

               Rate Standard   Direct
Standard 20533088/s       --     -18%
Direct   25184993/s      23%       --

Ideally, I'll close the gap a little more between the two but that's a problem for future me.

How It Works

  • Standard trampoline

    perl stack -> argument buffer (void **) -> trampoline -> function

    The trampoline is generic. It expects arguments to be laid out in a specific C array format.

  • Direct trampoline

    trampoline -> perl stack -> function

    infix generates machine code that calls directly into the perl API (e.g. SvIV) to fetch values from the Perl stack, puts them into CPU registers, and jumps straight to the target function. We cut out the middleman and reduces pointer indirection layers and memory reads. These are micro-optimizations that produce real world results.

Kitchen Reminders

  • Experimental

    This feature is currently marked as experimental because it is not as feature complete as the mainline infix trampolines and Affix's standard functions. It supports primitives (Int, Float, Pointer) robustly, but complex aggregates (Structs by value) may fallback to standard marshalling or behave unexpectedly in edge cases.

  • Restrictions

    Direct marshalling relies on knowing exact types at compile time. ...the JIT's compile time not application compile time. It's less forgiving of type mismatches (like passing a Struct where an Int is expected) than the standard path, which often attempts coercion.

Chapter 15: Smart Enums
4 min

C headers are full of enum definitions. To the C compiler, these are just integers. To the programmer, they have semantic meaning. When binding these functions to Perl, passing magic numbers (like 0, 1, 2) makes your code unreadable. Affix solves this by allowing you to define Enums that generate Perl constants and return dualvars.

The Recipe

Let's wrap a hypothetical state machine library.

use v5.40;
use Affix qw[:all];
use Affix::Build;

# 1. Compile C
my $c = Affix::Build->new();
$c->add( \<<~'END', lang => 'c' );
    typedef enum {
        STATE_IDLE     = 0,
        STATE_RUNNING,
        STATE_PAUSED,
        STATE_ERROR    = 99,
        // Bitmasks
        FLAG_READ      = 1 << 0,  // 1
        FLAG_WRITE     = 1 << 1,  // 2
        FLAG_RDWR      = FLAG_READ | FLAG_WRITE // 3
    } MachineState;

    int set_state(MachineState s) {
        return s; // Returns the new state
    }
END
my $lib = $c->link;

# 2. Define the Enum
# We can use:
#   'NAME'            -> Auto-increments
#   [ NAME => Value ] -> Explicit value
#   [ NAME => 'Expr'] -> C-style expression strings
typedef State => Enum [
    [ STATE_IDLE => 0 ],                           # 0
    'STATE_RUNNING',                               # Auto-set to 1
    'STATE_PAUSED',                                # Auto-set to 2
    [ STATE_ERROR => 99 ],                         # Manually set to 99
    [ FLAG_READ   => '1 << 0' ],                   # Calculated value is 1
    [ FLAG_WRITE  => '1 << 1' ],                   # Calculated value is 2
    [ FLAG_RDWR   => 'FLAG_READ | FLAG_WRITE' ]    # Calculated value is 3
];

# 3. Bind
# We use the typedef alias '@State'
affix $lib, 'set_state', [ State() ] => State();

# 4. Use Constants (Input)
# typedef installs these constants into your package.
set_state( STATE_RUNNING() );
set_state( FLAG_RDWR() );

# 5. Use Dualvars (Output)
# The return value behaves like a number AND a string.
my $current = set_state( STATE_PAUSED() );
if ( $current == 2 ) {
    say 'Numeric check passed.';
}
if ( $current eq 'STATE_PAUSED' ) {
    say 'String check passed.';
}

# Debugging is free
say sprintf 'Current State: %s (%d)', $current, $current;    # Prints "Current State: STATE_PAUSED (2)"

How It Works

  • 1. Constants Generation

    When you call typedef ... Enum [...], Affix calculates the integer value of every element in the list. When you pass that to typedef, Affix installs a constant subroutine (e.g., sub STATE_IDLE () { 0 }) into your current package. This allows you to use the names directly in your Perl code.

  • 2. Expression parsing

    Affix includes a small expression parser (see shunting yard algorithm). This allows you to copy and paste definitions from C headers like bit shifts (1 << 4) or combinations (FLAG_A | FLAG_B) directly into your Affix definition without manually calculating the result.

  • 3. Dualvars

    When a C function returns a value defined as an Enum type, Affix looks up the integer in the definition map. If found, it returns a Scalar with both slots filled:

    • IV (Integer Value): 2
    • PV (String Value): "STATE_PAUSED"

    This allows you to write readable code (eq 'STATE_...') while maintaining high performance for numeric comparisons.

Kitchen Reminders

  • Unknown values

    If the C library returns an integer that you didn't define in your Enum list (e.g., a new error code added in a library update), Affix simply returns the integer.

  • Scope

    The constants are installed into the package where typedef is called. If you put your Affix setup in a utility module (e.g., Acme::LibSample), callers will need to import those constants or refer to them fully qualified (Acme::LibSample::STATE_IDLE).

Chapter 14: Callbacks, Context, & Closures
2 min

In Chapter 12, we passed a simple callback. But real-world C libraries often take a callback and an opaque pointer (usually something like void * user_data) which allows you to store context data. However, if the library's author decided not to provide that, Affix allows you to bake the context into the callback itself using closures.

The Recipe

We will wrap a hypothetical iteration function that takes a callback but no data argument.

use v5.40;
use feature 'class';
no warnings 'experimental::class';
use Affix qw[:all];
use Affix::Build;

# 1. C Library
my $c = Affix::Build->new();
$c->add( \<<~'END', lang => 'c' );
    void iterate_3( void (*cb)(int) ) {
        cb(1); cb(2); cb(3);
    }
END
my $lib = $c->link;
affix $lib, 'iterate_3', [ Callback [ [Int] => Void ] ] => Void;

# 2. The Problem
# We want to sum these numbers into a Perl object.
# But the C function doesn't give us a slot to hand it the object directly.
class Sum {
    field $total = 0;
    method add($n) { $total += $n; }
    method get()   { $total; }
}
my $obj = Sum->new;

# 3. The Closure Solution
# We define the callback inside the scope where $obj exists.
# Perl closes over $obj.
my $cb = sub ($n) { $obj->add($n); };

# 4. Execute
iterate_3($cb);
say 'Total: ' . $obj->get();    # 6

How It Works

  • 1. Closures

    When you create sub { ... } in Perl, it captures the surrounding lexical variables ($obj). Event the new perlclass objects are just SV*s at their core.

  • 2. The trampoline

    When you pass $cb to Affix, infix generates a reverse trampoline. This is a C function pointer that exists just to bounce arguments back to us. When C calls it, the trampoline invokes the specific Perl CODE reference (CV*), which restores the Perl environment and finds $obj.

    To the C library, it looks like a standard function pointer. To you, it looks like magic.

Chapter 13: Transparent Structs
3 min

Sometimes you need to peek inside the oven. If a function expects a C style struct to be passed by value or if you want to read struct members without writing C helper functions, you can define the struct layout in Perl and Affix handles the rest.

The Recipe

We will create a simple geometry library that operates on Points and Rectangles.

use v5.40;
use Affix qw[:all];
use Affix::Build;

# 1. Compile C Library
my $c = Affix::Build->new();
$c->add( \<<~'END', lang => 'c' );
    typedef struct {
        int x, y;
    } Point;

    typedef struct {
        Point top_left;
        Point bottom_right;
        int color;
    } Rect;

    // Pass by Value
    int area(Rect r) {
        int w = r.bottom_right.x - r.top_left.x;
        int h = r.bottom_right.y - r.top_left.y;
        return w * h;
    }

    // Pass by Reference (Modify in place)
    void move_rect(Rect *r, int dx, int dy) {
        r->top_left.x += dx;
        r->top_left.y += dy;
        r->bottom_right.x += dx;
        r->bottom_right.y += dy;
    }
END
my $lib = $c->link;

# 2. Define Types
# Order matters! Defined in the same order as the C struct.
typedef Point => Struct [ x => Int, y => Int ];

# We can nest types. Point() refers to the typedef above.
typedef Rect => Struct [ tl => Point(), br => Point(), color => Int ];

# 3. Bind
affix $lib, 'area',      [ Rect() ]                       => Int;
affix $lib, 'move_rect', [ Pointer [ Rect() ], Int, Int ] => Void;

# 4. Use (Pass by Value)
# We represent the struct as a hash reference.
my $r = { tl => { x => 0, y => 0 }, br => { x => 10, y => 20 }, color => 0xFF00FF };
say 'Area: ' . area($r);    # 200

# 5. Use (Pass by Reference)
# We pass a reference to the hash.
# Affix updates the hash members after the call.
move_rect( $r, 5, 5 );
say "New TL: $r->{tl}{x}, $r->{tl}{y}";    # 5, 5

How It Works

  • 1. Structural typing

    typedef Point => Struct [ x => Int, y => Int ];
    

    Affix (by way of infix's type introspection) calculates the memory layout (padding, alignment, size) of the struct matching the platform's C ABI.

  • 2. Hashref marshalling

    When you pass a hashref (HV*) to a Struct argument, Affix packs the hash values into a temporary C memory block. When the call returns, if you passed a reference (as in step 5), Affix automagically unpacks the C memory back into your hashref, seemingly updating any changed values.

  • 3. Nesting

    Structs can contain other Structs, Arrays, and Pointers. Affix handles the recursive packing and unpacking automatically.

Kitchen Reminders

  • Performance

    Marshalling a deep hashref into a C struct (and back) involves copying data. For high-performance scenarios (like processing millions of points), use calloc to allocate the struct in C memory once, and pass the pointer around.

  • Missing keys

    If your hashref is missing a key defined in the Struct's list of fields, Affix assumes 0 (or NULL).

Chapter 12: Callbacks
3 min

C libraries often use callback functions to delegate logic back to the user. The standard library's qsort is the classic example: it knows how to sort, but it doesn't know how you'd like to compare your data. Affix allows you to pass a standard Perl subroutine (CodeRef) where C expects a function pointer.

The Recipe

Let's sort a list of integers using qsort.

use v5.40;
use Affix;

# 1. Bind qsort
# void qsort(void *base, size_t nmemb, size_t size,
#            int (*compar)(const void *, const void *));
#
# The callback signature is defined inside the argument list.
# Callback[ [Args...] => ReturnType ]
affix libc, 'qsort', [
    Pointer [Int], Size_t, Size_t,    
    Callback [ [ Pointer [Int], Pointer [Int] ] => Int ]
] => Void;

# 2. Prepare Data
# qsort works on a raw memory array.
# We create a C array of integers.
my @nums  = ( 88, 56, 100, 2, 25 );
my $count = scalar @nums;

# 3. Define the Comparator
# C passes us pointers to the two items being compared.
# We must dereference them to get the values.
my $compare_fn = sub ( $p_a, $p_b ) { $$p_a <=> $$p_b };

# 4. Call
# We pass the ArrayRef directly. Affix handles the pointer decay
# and write-back (see Chapter 10).
# sizeof(int) is usually 4.
qsort( \@nums, $count, 4, $compare_fn );
say join ', ', @nums;    # 2, 25, 56, 88, 100

How It Works

  • 1. The Callback type

    Callback[ [Pointer[Int], Pointer[Int]] => Int ]
    

    This tells Affix to create a reverse trampoline. It generates a small piece of C code that looks like a standard C function. When that C code is called by qsort, it:

    1. Marshals the C arguments (two pointers here) into Perl variables (SV*).
    2. Calls your Perl subroutine $compare_fn.
    3. Takes the integer return value and passes it back to C.
  • 2. Pointer dereferencing

    $$p_a <=> $$p_b;
    

    In the callback signature, we claimed the arguments were Pointer[Int]. Affix receives the raw address from C, wraps it in a pin with the type Int, and passes it to your sub. Dereferencing it reads the integer value from memory.

Kitchen Reminders

  • Scope and lifecycle

    The magic trampoline created for your subroutine exists only as long as the C call is running. If you pass a callback to a C function that stores it for later use (like setting an event handler in a GUI library), you must ensure your Perl CodeRef stays alive.

  • Exceptions

    If your Perl callback throws an exception (croak, die, etc.), Affix catches it, issues a warning, and returns a zero/void value to C to prevent crashing the host application. C does not understand Perl exceptions and the C function might not understand this either. Be careful!

Chapter 11: Variadic Functions
4 min

Some C functions don't have a fixed number of arguments. The most famous example is printf, which takes a format string and... almost everything else. Affix supports variadic functions, but they require a little more care than standard functions because the C compiler isn't there to cast types for you.

The Recipe

We will wrap the standard C library's snprintf. This is safer than printf because it writes to a buffer.

We also handle a common cross-platform annoyance: on Windows, this function is often named _snprintf, while on Linux/macOS it is snprintf. We can normalize this using symbol aliasing.

use v5.40;
use Affix;

# 1. Bind snprintf
# Windows uses _snprintf, POSIX uses snprintf.
# We detect the OS and bind the correct symbol to the name 'snprintf'.
my $symbol = ( $^O eq 'MSWin32' ? '_' : '' ) . 'snprintf';

# Signature: int snprintf(char *str, size_t size, const char *format, ...);
# We use '...' (or VarArgs) to indicate the variadic part.
affix libc, [ $symbol => 'snprintf' ], [ Pointer [Char], Int, String, VarArgs ] => Int;

# 2. Prepare a buffer
# We need a place for C to write the formatted string.
my $size   = 100;
my $buffer = "\0" x $size;

# 3. Call with Inference
# Affix guesses the C type based on the Perl value.
#   "A String" -> char*
#   123        -> int64_t
#   12.34      -> double
snprintf( $buffer, $size, 'Name: %s, ID: %d, Score: %.2f', 'Alice', 42, 99.9 );

# Clean up the null terminator and print
$buffer =~ s/\0.*//;
say $buffer;    # "Name: Alice, ID: 42, Score: 99.90"

# 4. Explicit Coercion
# Sometimes inference isn't enough.
# Here we want to print a pointer address (%p).
# Passing a Perl integer might be interpreted as a value, not a pointer.
# We use coerce() to force specific types.
my $ptr = 0xDEADBEEF;
snprintf( $buffer, $size, 'Pointer address: %p', coerce( Pointer [Void], $ptr ) );
say $buffer =~ s/\0.*//r;

How It Works

  • 1. Symbol aliasing

    affix $lib, [ 'RealSymbol', 'PerlName' ], ...
    

    Often, C libraries have messy naming conventions or platform-specific quirks (like _snprintf vs snprintf), or the library has mangled symbol names. By passing an array reference as the symbol name, you tell Affix: "Find RealSymbol in the library, but create a Perl subroutine named PerlName."

  • 2. VarArgs in signature

    [ ..., VarArgs ]
    

    The VarArgs constant (or the string ';') tells Affix: "Stop checking types here. Everything after this point is dynamic."

  • 3. Dynamic JIT

    When you call a variadic function, Affix looks at the arguments you provided at that moment. It generates a custom, temporary infix style signature on the fly (e.g., (*char, size_t, *char, *char, int64, double)->int) and compiles a trampoline for it.

    These trampolines are cached, so calling snprintf repeatedly with the same argument types is fast.

  • 4. Coercion

    coerce( Type, $value )
    

    If you need to pass a float (not double), a short, or a struct by value, the default inference won't work. coerce attaches a "Type Hint" to the value. Affix sees this hint and uses the exact type you requested.

Kitchen Reminders

  • Format strings

    Affix does not validate your format string (e.g. "%s"). If you pass an integer to a %s placeholder, snprintf will assume it's a pointer address and try to read memory from it which will likely result in a segfault.

Chapter 10: Mutable Arrays
3 min

In C, arrays passed to functions are often used as output buffers. The function reads data from the array, modifies it in place, and expects the caller to see the changes. In XS, these are defined as IN_OUT parameters. Affix supports this functionality automatically.

The Recipe

We will define a C function that reverses an integer array in place.

use v5.40;
use Affix qw[:all];
use Affix::Build;

# 1. Compile C
my $c = Affix::Build->new();
$c->add( \<<~'END', lang => 'c' );
    // Reverses array 'a' in place
    int array_reverse(int a[], int len) {
        int tmp, i;
        int ret = 0; // Sum of first half (arbitrary logic)
        for (i = 0; i < len / 2; i++) {
            ret += a[i];
            tmp = a[i];
            a[i] = a[len - i - 1];
            a[len - i - 1] = tmp;
        }
        return ret;
    }

    // Wrapper for fixed-size 10-element array
    int array_reverse10(int a[10]) {
        return array_reverse(a, 10);
    }
END
my $lib = $c->link;

# 2. Bind
# Dynamic length array
affix $lib, array_reverse => [ Array[Int], Int ], Int;

# Fixed length array
affix $lib, array_reverse10 => [ Array[Int, 10] ], Int;

# 3. Call (Fixed Size)
my @a = ( 1 .. 10 );
array_reverse10( \@a );
say "@a"; # Output: 10 9 8 7 6 5 4 3 2 1

# 4. Call (Dynamic Size)
my $b = [ 1 .. 20 ];
array_reverse( $b, 20 );
say "@$b"; # Output: 20 19 ... 2 1

How It Works

  • 1. In-Place Modification

    When you pass a Perl ArrayRef to an Array argument:

    1. Affix allocates a temporary C array.
    2. Copies your data into it.
    3. Calls the C function.
    4. Copies the data back from C to your Perl array.

    This ensures that any changes made by the C function are reflected in your SV*.

  • 2. Fixed size padding

    affix ..., [ Array[Int, 10] ] ...
    

    If you define a fixed size (e.g. 10) but pass a shorter list (e.g. [1, 2]), Affix allocates the full 10 integers and zero-fills the rest. This protects the C function from reading garbage memory if it assumes the array is 10 elements long.

Kitchen Reminders

  • Performance

    Copying arrays back and forth (Perl -> C -> Perl) takes time. For very large datasets where you only need to modify a few bytes, consider using malloc and Pointer types to keep the data in C memory (see Chapter 7).

  • Reference requirement

    You must pass a reference (\@a or $array_ref). Passing a list directly (e.g. array_reverse([1,2], ...)) works, but the modifications will be lost because the anonymous array reference is discarded immediately after the call. This is a limitation of perl's handling of scalars.

Chapter 9: Arrays vs. Pointers
3 min

In C, arrays and pointers are cousins. In function arguments, they are twins. Declaring void func(int a[]) is exactly the same as void func(int *a). However, in Perl and thus in Affix, this isn't the case.

  • Pointer[Int] expects a reference to a scalar integer (memory address).
  • Array[Int] expects a list of integers (data).

This recipe shows how to pass lists of data to C using the Array type, which handles allocation and copying for you.

The Recipe

We will wrap a C function that sums a zero-terminated array of integers.

use v5.40;
use Affix qw[:all];
use Affix::Build;

# 1. Compile C
my $c = Affix::Build->new();
$c->add( \<<~'END', lang => 'c' );
    #include <stdlib.h>

    int array_sum(const int * a) {
        int i, sum;
        if (a == NULL)
            return -1; // Error code for NULL

        // Loop until we hit a 0
        for (i = 0, sum = 0; a[i] != 0; i++)
            sum += a[i];
        return sum;
    }
END

# 2. Bind
# We use Array[Int] to tell Affix to expect a list of numbers.
# Affix handles the C "array decay" (passing as pointer) automatically.
affix $c->link, array_sum => [ Array [Int] ] => Int;

# 3. Call
# Pass undef -> NULL
say "Undef: " . array_sum(undef);    # -1

# Pass [0] -> {0} -> Sums to 0 (Immediate stop)
say "Zero:  " . array_sum( [0] );    # 0

# Pass List -> {1, 2, 3, 0} -> Sums to 6
say "List:  " . array_sum( [ 1, 2, 3, 0 ] );    # 6

How It Works

  • 1. Array marshalling

    array_sum( [ 1, 2, 3, 0 ] );
    

    When you define an argument as Array[Int], Affix allocates a temporary C array, copies the values from your Perl array reference into it, and passes the pointer to the C function. This matches C's array decay rules.

  • 2. The undef case

    array_sum(undef);
    

    C allows array pointers to be NULL. Affix supports this by checking for undef and passing NULL to the function, skipping the allocation entirely.

Kitchen Reminders

  • Syntactic sugar

    You could have bound this function using Pointer[Int]. If you did, you would have to manually pack a binary string or use malloc to create the buffer. Array[Int] handles that boilerplate for you.

  • Sentinels

    Notice we passed 0 at the end of our list: [1, 2, 3, 0]. Affix does not automatically append null terminators to numeric arrays (unlike Strings). Since our C function relies on finding a 0 to stop the loop, we must provide it explicitly. I could have designed this demonstration to accept a Size_t array length but this seemed like a better lesson to sneak in here.

Chapter 8: System Libraries on Linux
4 min

Perl scripts often live in the terminal or deep inside a server handing out webpages or whatnot, but they don't have to stay there. Earlier, in chapter 2, we demonstrated using system libraries on Windows to display a simple MessageBox. This time, we turn to Linux (and BSDs with a desktop environment).

The libnotify library allows applications to send pop-up notifications to the user. These are the bubbles you see from apps like Slack, Discord, or your system updater.

Notifications from Affix!

This library uses the GLib object system (GObject), which is notoriously complex to bind manually. However, if we only want to trigger a notification, we can treat the objects as opaque pointers and ignore the internal complexity entirely.

The Recipe

use v5.40;
use Affix;

# 1. Find the library
# locate_lib searches system paths (like /usr/lib) for libnotify.so
# It returns the full path as a string.
my $libnotify = Affix::locate_lib('notify');
unless ($libnotify) {
    die "Could not find libnotify. Are you on Linux with libnotify-bin installed?";
}

# 2. Bind the lifecycle functions
# bool notify_init(const char *app_name);
affix $libnotify, 'notify_init',   [String] => Bool;
affix $libnotify, 'notify_uninit', []       => Void;

# 3. Bind the object functions
# NotifyNotification* notify_notification_new(const char *summary, const char *body, const char *icon);
affix $libnotify, 'notify_notification_new', [ String, String, String ] => Pointer [Void];

# bool notify_notification_show(NotifyNotification *notification, GError **error);
affix $libnotify, 'notify_notification_show', [ Pointer [Void], Pointer [Void] ] => Bool;

# 4. Use it
# Initialize the library with our app name
notify_init('Affix');

# Create the notification object
# We get back a Pin (opaque pointer), but we don't need to look inside it.
my $n = notify_notification_new(
    'Keep your stick on the ice! 🏒',                          #
    "Hello from Affix!\nWelcome to the fun world of FFI.",    #
    'dialog-information'                                      # Standard icon name
);

# Show it
# We pass undef for the second argument (GError**), meaning "ignore errors".
notify_notification_show( $n, undef );

# Clean up
notify_uninit();

How It Works

  • 1. Locating libraries

    my $libnotify = Affix::locate_lib('notify');
    

    Unlike load_library (which loads the lib into memory immediately), locate_lib scans the system's dynamic linker paths (like LD_LIBRARY_PATH) and simply returns the filename. We pass this path to affix, which handles the loading.

  • 2. Ignoring complexity

    The notify_notification_new function returns a NotifyNotification*. In C, this is a struct with many private fields and GObject metadata. In Perl, we don't care. We define the return type as Pointer[Void].

    As long as we pass that pointer back to other functions in the same library (like notify_notification_show), everything works.

  • 3. Optional Error Handling

    The C signature for show is:

    gboolean notify_notification_show (NotifyNotification *notification, GError **error);
    

    The second argument is a pointer-to-a-pointer that the library fills if an error occurs. By passing undef, Affix sends a NULL, telling the library we don't want to receive error details.

Kitchen Reminders

  • Platform specific

    This recipe works on Linux/BSD with a freedesktop.org compliant notification daemon (GNOME, KDE, XFCE, etc.). It will fail to find the library on Windows or vanilla macOS.

  • Unicode

    Notice we used an emoji in the notification. Because we used the String type, Affix automatically encoded this as UTF-8, which is what modern Linux libraries expect.

Chapter 7: Manual Memory Management
3 min

Perl handles memory for you. C does not. When you start allocating raw memory buffers in Affix, you are stepping into the C world. This recipe demonstrates how to manually allocate, manipulate, and free memory using the standard C lifecycle.

The Recipe

use v5.40;
use Affix qw[:all];

# 1. Allocation
# We allocate 14 bytes.
# malloc returns a Pointer[Void] (a pin).
my $void_ptr = malloc( 14 );

# 2. String Duplication
# strdup allocates new memory containing a copy of the string.
my $ptr_string = strdup("hello there!!\n");

# 3. Memory Copy
# We copy 15 bytes (text + null terminator) from one pointer to another.
# memcpy works on raw pointers, so passing $void_ptr is fine.
memcpy( $void_ptr, $ptr_string, 15 );

# 4. Read (The Cast)
# Dereferencing $void_ptr ($$void_ptr) would just return the memory address
# as an integer, because Affix doesn't know what's inside.
# We must CAST it to a specific type to read it.
my $str_view = cast( $void_ptr, String );

print $str_view;

# 5. Cleanup
# These are managed pointers, so Perl *would* free them when the variables
# go out of scope. But in C, explicit is better than implicit.
free($ptr_string);
free($void_ptr);

How It Works

  • 1. malloc returns an opaque pointer

    my $void_ptr = malloc( 14 );
    

    Just like in C, malloc returns a void*. It represents a chunk of raw memory with no type information attached.

  • 2. cast adds meaning

    my $str_view = cast( $void_ptr, String );
    

    Casting creates a new pin that points to the same memory address but has different type metadata.

    $void_ptr  -> Address 0x1234, Type: *void
    $str_view  -> Address 0x1234, Type: *char
    

    When you dereference $$str_view, Affix sees the type is String and reads the memory until the null terminator.

  • 3. Pointer arithmetic via memcpy

    memcpy( $void_ptr, $ptr_string, 15 );
    

    This is a raw memory operation. It doesn't care about types; it just moves bytes. This is extremely fast, but dangerous if you get the length wrong. Always ensure your destination buffer is large enough to hold the source data.

  • 4. Explicit free

    free($void_ptr);
    

    Affix uses Perl's memory allocator (safemalloc) for malloc, calloc, and strdup. This means these pointers are distinct from pointers allocated by a loaded C library (which use the system malloc).

    Always use Affix's free for Affix-created pointers, and the library's free function for library-created pointers. You must keep them seperate!

Kitchen Reminders

  • The void * Default

    If you see a large integer when you expected data (e.g. 2029945...), you are dereferencing a Pointer[Void]. Use cast($ptr, Type) to tell Affix how to read the data.

  • Buffer Overflows

    Perl aims to protect you from buffer overflows. Affix does not. memcpy will happily write past the end of your allocation and corrupt your program's memory. Measure twice, cut once. Yes, that's a carpentry reference in a cookbook. It's fine.

Chapter 6: Opaque Pointers
4 min

Affix cannot directly instantiate a C++ class or read a C struct unless we define every single field with the correct type and in the proper order. Often, we don't care about the fields, we just want to hold onto the object and pass it back to the library later. In this case, we can treat the object as a black box, an opaque pointer (Pointer[Void]) and use helper functions to interact with it.

The Recipe

We will create a simple C++ Person struct, wrap it with C-compatible helpers, and bind it to Perl.

use v5.40;
use Affix qw[:all];
use Affix::Build;

# 1. Compile the C++ Library
my $c = Affix::Build->new();
$c->add( \<<~'END', lang => 'cpp' );
    #include <stdlib.h>
    #include <string.h>

    typedef struct {
        char * name;
        unsigned int age;
    } Person;

    // extern "C" prevents C++ name mangling. This ensures the symbols in the
    // DLL are just "person_new", not "_Z10person_newPKcj".
    extern "C" {
        Person * person_new(const char * name, unsigned int age) {
            Person * self = (Person *) malloc(sizeof(Person));
            self->name = strdup(name);
            self->age = age;
            return self;
        }

        const char * person_name(Person * self) {
            return self->name;
        }

        unsigned int person_age(Person * self) {
            return self->age;
        }

        void person_free(Person * self) {
            if (self) {
                free(self->name);
                free(self);
            }
        }
    }
END
my $lib = $c->link;

# 2. Define the Opaque Type
# We tell Affix: "Whenever you see 'Person', treat it as a void pointer."
typedef Person => Pointer [Void];

# 3. Bind
# Notice we use Person() as the type.
affix $lib, 'person_new',  [ String, UInt ] => Person();
affix $lib, 'person_name', [ Person() ]     => String;
affix $lib, 'person_age',  [ Person() ]     => UInt;
affix $lib, 'person_free', [ Person() ]     => Void;

# 4. Use
my $roger = person_new( 'Roger Frooble Bits', 35 );

# $roger is just a blessed scalar holding a memory address.
# We pass it back to C to get data.
say 'Name: ' . person_name($roger);
say 'Age:  ' . person_age($roger);

# Clean up
person_free($roger);

How It Works

  • 1. extern "C"

    C++ compilers "mangle" function names (e.g., person_new might become _Z10person_newPKcj) to support function overloading. This is a nightmare to bind and depends on the ABI of the compiler. Wrapping your helper functions in extern "C" forces compilers to use standard C names.

  • 2. The shim pattern

    We didn't bind the C++ struct fields directly. Instead, we wrote small C helper functions (shims) to access the data:

    const char * person_name(Person * self) { return self->name; }
    

    This insulates you from changes in the C++ class layout.

  • 3. Typedef aliases

    typedef Person => Pointer[Void];
    

    This creates a named alias. While Pointer[Void] works, using Person() in your signatures makes the code self-documenting. If you later define Car => Pointer[Void], Affix won't stop you from passing a Car to a Person function (since they are both just pointers), but your code will be much easier to read and debug.

Kitchen Reminders

  • Constructors must return

    It seems obvious but, in C++, forgetting to return self; in a constructor function is easy to do and leads to immediate segfaults or illegal instruction errors.

  • Memory ownership

    Since person_new used malloc, the memory belongs to the C heap. You must explicitly call person_free to release it. Affix cannot garbage collect these opaque pointers automatically unless you attach a destructor using Perl's DESTROY (which is an advanced topic I might cover in another chapter).

Chapter 5: The Standard Library
2 min

Every operating system comes with a "C standard library" or, simply, libc. It contains the fundamental building blocks of C programming: memory allocation, file I/O, string manipulation, system calls, and much more.

Affix provides a shortcut to access this library without you needing to know its exact filename (which varies wildly between Linux, macOS, and Windows).

The Recipe

We will bind the standard C function puts which takes a string, prints it to stdout, and appends a newline character.

use v5.40;
use Affix;

# 1. Bind
# libc is a helper function exported by Affix.
# It returns the handle to the system's standard library.
affix libc, 'puts' => [String] => Int;

# 2. Call
# Note: puts() automatically adds a newline "\n"
puts('Hello World');

# 3. Check Result
# puts returns a non-negative integer on success, or EOF (-1) on error.
my $ret = puts('Affix makes FFI easy.');
say 'Successfully wrote to stdout.' if $ret >= 0;

How It Works

  • 1. The libc Helper

    Finding the standard library is platform-dependent:

    • Linux: libc.so.6 (usually)
    • macOS: libSystem.B.dylib
    • Windows: msvcrt.dll (or ucrtbase.dll)

    The libc function (exported by Affix) handles this detection logic for you, returning the correct handle for your operating system.

  • 2. The puts Function

    The C signature for puts is:

    int puts(const char *s);
    

    We map this to:

    [String] => Int
    

    When you call puts('Hello'), Affix takes your Perl string, ensures it is null-terminated, and passes the pointer to C. The C library writes the bytes to the file descriptor for STDOUT.

Kitchen Reminders

  • Output Buffering

    Perl and C use their own IO buffers. If you mix print "...", say "...", and puts(...) in the same script, the output might appear out of order depending on when the buffers are flushed. Setting $| = 1; in Perl usually helps, but doesn't strictly control the C buffer.

  • Implicit Newlines

    Unlike Perl's print, puts always adds a newline. If you want to print without a newline using C, you would need to bind printf.

Chapter 4: Strings and Static State
4 min

Dealing with C strings usually involves asking "Who owns this memory?"

In Perl, strings are values. In C, they are pointers. A common (though not thread-safe) pattern in C libraries is to return a pointer to an internal static buffer. This buffer is overwritten or freed the next time the function is called.

If you were using raw pointers, this would be dangerous; your Perl variable would suddenly change its value or point to freed memory when you made a subsequent API call.

Affix solves this by copying string return values into Perl's memory space immediately.

The Recipe

In this example, we implement a C function that remembers its last result. It frees the old result before allocating a new one. We use a final call with undef to trigger a cleanup.

use v5.40;
use Affix qw[:all];
use Affix::Build;

# 1. Compile the C library
my $c = Affix::Build->new();
$c->add( \<<~'END', lang => 'c' );
    #include <stdlib.h>
    #include <string.h>

    const char * string_reverse(const char * input) {
        // Static pointer persists across function calls
        static char * output = NULL;
        int i, len;

        // Cleanup previous call's memory
        if (output != NULL) {
            free(output);
            output = NULL;
        }

        // Handle "Cleanup Mode" (NULL input)
        if (input == NULL)
            return NULL;

        // Allocate new buffer
        len = strlen(input);
        output = malloc(len + 1);

        // Reverse the string
        for (i = 0; input[i]; i++)
            output[len - i - 1] = input[i];
        output[len] = '\0';

        return output;
    }
END

my $lib = $c->link;

# 2. Bind the function
# String means "const char*" for input and output.
affix $lib, 'string_reverse', [String] => String;

# 3. Use it
# Input:  "\nHello world"
# Output: "dlrow olleH\n"
print string_reverse("\nHello world");

# 4. Cleanup
# Passing undef sends NULL to C, triggering the free() logic.
string_reverse(undef);

How It Works

  • 1. Passing Strings (Input)

    [ String ] => ...
    

    When passing a Perl string to a String argument, Affix temporarily allocates a C buffer, copies the Perl string (encoded as UTF-8), and passes that pointer to C. This buffer is valid only for the duration of the call.

  • 2. Returning Strings (Output)

    ... => String
    

    When a C function returns a String type, Affix reads the memory at that address (up to the first null byte), creates a new Perl scalar containing those bytes, and returns the scalar.

    It does not return a pointer to the C memory.

    This is crucial for this recipe. Even though the C function eventually calls free(output) on the next invocation, our Perl variable holding "dlrow olleH\n" remains safe because it is a distinct copy.

  • 3. Passing Undef

    string_reverse(undef);
    

    For string arguments, undef is marshalled as NULL. Our C function checks for this to perform its cleanup duties.

Kitchen Reminders

  • Copy vs. Reference

    If you want a copy of the text (standard Perl behavior), use the type String. If you want to modify the C memory buffer in place or hold onto the specific memory address, use Pointer[Char] (see Chapter 3).

  • Thread Safety

    While Affix is thread-safe, C functions using static variables (like the one in this recipe) are generally not thread-safe. If two threads call string_reverse at the same time, they will race to free/overwrite the same static pointer.

Chapter 3: Binary Data and the Null Byte Trap
3 min

C strings are simple: they start at a memory address and end at the first zero/null byte (\0). But what if your data contains nulls? If you use standard string types like String or Array[Char], C (and Affix) will assume the data ends at the first null byte. This is fatal for encryption, compression, and binary formats.

To handle this, we must stop thinking in characters and start thinking in bytes.

The Recipe

This is a demonstration of the XOR cipher. Since the data might contain nulls, we handle it as a raw buffer.

use v5.40;
use Affix qw[:all];
use Affix::Build;

# 1. Compile the C library
my $c = Affix::Build->new();
$c->add( \<<~'END', lang => 'c' );
    #include <stdlib.h>
    #include <string.h>

    char * string_crypt(const char * input, int len, const char * key) {
        char * output = malloc(len + 1);
        if (!output) return NULL;
        output[len] = '\0';

        int key_len = strlen(key);
        for (int i = 0; i < len; i++) {
            output[i] = input[i] ^ key[i % key_len];
        }
        return output;
    }

    void string_free(void * p) { free(p); }
END

my $lib = $c->link;

# 2. Bind the functions
# Return Pointer[Void] to get the raw address (Pin) without interpretation.
affix $lib, 'string_crypt', [ String, Int, String ] => Pointer[Void];
affix $lib, 'string_free',  [ Pointer[Void] ]       => Void;

# 3. The Wrapper
sub cipher( $input, $key ) {
    my $len = length($input);

    # Call C. We get back a Pin (unmanaged pointer).
    my $ptr = string_crypt( $input, $len, $key );
    return undef if is_null($ptr);

    # Create a view of the memory as an Array of Bytes (UInt8).
    # Important:
    #   Array[Char]  => Reads up to NULL (String behavior)
    #   Array[UInt8] => Reads exactly $len bytes (Binary behavior)
    my $view = cast( $ptr, Array[UInt8, $len] );

    # Dereference the view ($$view).
    # For byte arrays, Affix returns a binary string directly.
    my $binary_string = $$view;

    # Clean up the C memory immediately
    string_free($ptr);

    return $binary_string;
}

# 4. Run it
my $orig = 'hello world!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!';
my $key  = 'foobar';

my $enc = cipher( $orig, $key );
my $dec = cipher( $enc,  $key );

say "Original:  $orig";
say "Decrypted: $dec";

# Verify the binary data size
say "Encrypted length: " . length($enc);

use JSON::PP;
say "Encrypted blob:   " . encode_json([$enc]);

How It Works

  • 1. Pointer[Void]

    We define the return type as Pointer[Void] to prevent Affix from automatically attempting to read it as a null-terminated string. We receive a raw Pin.

  • 2. Casting to Array[UInt8]

    my $view = cast( $ptr, Array[UInt8, $len] );
    

    We use UInt8 (unsigned 8-bit integer) instead of Char. This tells Affix we are dealing with raw binary data, not text. Affix will read exactly $len bytes from memory, regardless of nulls.

  • 3. Dereferencing

    my $binary_string = $$view;
    

    When you dereference a Pin defined as an Array of 8-bit types, Affix optimizes the operation. It copies the raw bytes directly into a Perl scalar.

Kitchen Reminders

  • Char vs UInt8

    In Affix, Array[Char] is treated as a string buffer (stops at null). Array[UInt8] is treated as a byte buffer (reads full length). Use the one matching your data.

Chapter 2: Speaking Windows
3 min

When interacting with the Windows API (Win32), you will inevitably face two facts: libraries are named things like user32.dll, and text is almost always expected to be UTF-16 (Wide Characters). In XS, bridging this gap involved tedious calls to MultiByteToWideChar and manual buffer management.

Affix handles the translation for you.

The Recipe

This recipe displays a native Windows message box containing Unicode characters (an emoji). It looks like this:

Screenshot 2025-12-23 152053

Okay, here's the sauce. ...the source:

use v5.40;
use utf8; # Required for literal emojis in source
use Affix;

# Define some standard Windows constants
use constant MB_OK                   => 0x00000000;
use constant MB_DEFAULT_DESKTOP_ONLY => 0x00020000;

# Bind the function
# Library: user32.dll
# Symbol:  MessageBoxW (The Unicode version)
# Alias:   MessageBox  (What we call it in Perl)
affix 'user32', [ MessageBoxW => 'MessageBox' ] =>
    [ Pointer [Void], WString, WString, UInt ] => Int;

# Call it
# undef becomes NULL (No owner window)
MessageBox( undef, 'Keep your stick on the ice.', '🏒', MB_OK | MB_DEFAULT_DESKTOP_ONLY );

How It Works

  • 1. The A vs. W Suffix

    Most Windows functions come in two flavors: ANSI (ending in A) and Wide (ending in W). MessageBoxA expects system-codepage strings (like ASCII), while MessageBoxW expects UTF-16.

    Since modern Perl is Unicode-aware, we almost always want the W version.

  • 2. Aliasing

    We bind the symbol MessageBoxW, but that is a clumsy name to type in Perl. By passing an array reference [ 'MessageBoxW' => 'MessageBox' ], we tell Affix to look up the real symbol in the DLL but install it into our namespace as MessageBox.

  • 3. The WString Type

    This is the magic ingredient.

    [ ..., WString, WString, ... ]
    

    When you pass a Perl string to a WString argument, Affix performs the following steps on the fly:

    1. Check the internal encoding of the Perl scalar.
    2. Transcode the string into UTF-16LE (Little Endian).
    3. Append a double-null terminator (\0\0).
    4. Pass the pointer to the C function.

    This allows us to pass the hockey stick emoji '🏒' directly.

  • 4. The Null Pointer

    The first argument to MessageBox is a handle to an owner window (HWND). Since we don't have a parent window, we pass NULL.

    In Affix, NULL is represented by Perl's undef. When passed to a Pointer type, undef is automatically converted to address 0x0.

Kitchen Reminders

  • Input Only

    The WString type is designed for input strings (LPCWSTR or const wchar_t*). Affix creates a temporary buffer for the duration of the call. If the C function intends to modify the string buffer, you must use Pointer[WChar] and manage the memory yourself using calloc.

  • use utf8

    If you are typing special characters like '🏒' directly into your Perl script, always remember use utf8; at the top of your file, or Perl might interpret the bytes incorrectly before Affix even sees them.

Chapter 1: The Instant C Library
4 min

Sometimes, you don't have a pre-existing .dll or .so file. Sometimes, you just have an idea, a heavy mathematical loop, or a snippet of C code you found on Stack Overflow, and you want to run it right now inside your Perl script.

This is where Affix::Build shines. It turns your Perl script into a build system, a linker, and a loader, all in about five lines of code.

Let's look at a classic "Hello World" of pointer manipulation: the Integer Swap.

The Recipe

use v5.40;
use Affix;
use Affix::Build;

# 1. Spin up the compiler
my $c = Affix::Build->new();

# 2. Add some C code directly into the script
$c->add( \<<~'', lang => 'c' );
    void swap(int *a, int *b) {
        int tmp = *b;
        *b = *a;
        *a = tmp;
    }

# 3. Compile, Link, and Bind
affix $c->link, swap => [ Pointer [Int], Pointer [Int] ] => Void;

# 4. Profit
my $a = 1;
my $b = 2;
say "Before: [a,b] = [$a, $b]";

# Pass references so C can write back to Perl
swap( \$a, \$b );
say "After:  [a,b] = [$a, $b]";

How It Works

This creates a seamless bridge between the two languages. Here is what is happening under the hood:

  1. The Builder

Affix::Build->new() creates a temporary workspace. It detects your operating system (Windows, Linux, macOS, BSD) and hunts for a viable compiler chain (GCC, Clang, MSVC). You don't need to configure Makefiles or worry about linker flags; the class handles the heavy lifting.

  1. The Source

We pass the source code as a reference to a string (\<<~''>). This tells the compiler, "I'm not giving you a filename; I'm giving you the raw code." We specify lang => 'c', but Affix::Build is a polyglot—it could just as easily have been cpp, rust, or fortran.

  1. The Link

Calling $c->link triggers the build process. It writes your source to a temp file, compiles it into an object file, and links it into a dynamic library. It returns the absolute path to that library (e.g., /tmp/affix_lib.so).

We pass that path immediately to affix( ... ), binding the symbol swap.

  1. The Types

This is the most critical part of this recipe:

[ Pointer [Int], Pointer [Int] ]

If we had defined this as [ Int, Int ], Affix would have passed the values 1 and 2 to C. The C code would have swapped those values in its own local stack and returned, leaving our Perl variables untouched.

By specifying that these are Pointers, we tell Affix we expect a memory address.

  1. The Execution

Because we defined the types as Pointers, we must pass B to our Perl scalars:

swap( \$a, \$b );

Affix takes the memory address of the value held in the scalar $a, passes it to C, and allows the C code to overwrite the memory directly. When swap returns, $a sees the data that was previously in $b and vice versa. Their values are swapped!

Kitchen Reminders

The chef would like to remind you of things you should have noted or learned from this recipe.

  • Compiler Required

While Affix works on any machine, Affix::Build obviously requires a C compiler (like gcc, clang, or Visual Studio) to be in your system's PATH.

  • Automatic Cleanup

By default, Affix::Build creates a temporary directory for the build artifacts. When your script ends, that directory is wiped clean. If you want to inspect the generated C files or the .so library for debugging, pass clean => 0 to the constructor:

my $c = Affix::Build->new( clean => 0, debug => 1 );
  • Type Safety

C is unforgiving. If you tell Affix the argument is a Pointer[Int], but you pass a string like "Hello", Affix will try to make it work, but the C code effectively interprets the bytes of that string as an integer. Always match your Perl data types to your Affix definitions.

Welcome to the Affix Cookbook
2 min

For decades, the barrier between Perl and the raw speed of C (or Rust, or C++, or Fortran) has been guarded by the complex, arcane rituals of XS. You had to learn a new macro language, configure a build system, handle the Perl stack manually, and compile everything before you could even run a "Hello World".

Affix changes the menu.

Affix is a Foreign Function Interface (FFI) built on the low-overhead infix JIT engine. It allows you to bind libraries, marshal complex data structures, and manage memory dynamically, all from within your Perl script.

What is in this Cookbook?

This document is a collection of recipes designed to take you from a hungry beginner to a master chef of systems programming.

We will cover:

  • The Basics: Calling standard C library functions like printf and pow.

  • The Compiler: Using Affix::Compiler to write C code directly inside your Perl scripts for instant optimization.

  • Memory Management: Understanding Pointers, Pins, and how to allocate memory without leaking it.

  • Advanced Structures: Handling C Structs, Unions, and Arrays as easily as Perl Hashes and Lists.

  • The Exotic: Callbacks, Function Pointers, and SIMD Vector mathematics.

In some articles, I'll be including benchmarks, the majority of results presented will be taken from my system running Windows 11 Pro on an AMD Ryzen 7 7840HS with 32Gb of RAM, and the most recent release of Strawberry Perl (currently v5.42.0).

Prerequisites

To follow these recipes, you will need:

  • Perl v5.40 or higher. Affix leverages modern Perl features like class.

  • Affix installed from CPAN. (cpanm -v Affix)

  • A C compiler (GCC, Clang, or MSVC) is highly recommended for the advanced recipes, though not strictly required for binding to existing system libraries.

Grab your apron. Let's get cooking. (Tired of the cooking references yet?)

sanko/infix v0.1.3 C

This release contains real-world usage fixes since I'm using it in Affix.pm and not just experimenting with different JIT forms.

Changed

  • Updated JIT validation logic to explicitly reject incomplete forward declarations, preventing the creation of broken trampolines.

Fixed

  • 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). The descriptor is now closed immediately after mapping, as intended.
  • Fixed signature positioning cache. The error messages will now (probably) point exactly where things are broken.
  • Fixed a critical bug in the Type Registry where forward declarations (e.g., @Node;) did not create valid placeholder types, causing subsequent references (e.g., *@Node) to fail resolution with INFIX_CODE_UNRESOLVED_NAMED_TYPE.
  • Fixed infix_registry_print to explicitly include forward declarations in the output, improving introspection visibility.
  • Updated is_passed_by_reference in abi_win_x64.c to always return true for INFIX_TYPE_ARRAY. This ensures the prepare stage allocates 8 bytes (pointer size) and the generate stage emits a move of the pointer address, not the content.
  • Updated prepare_forward_call_frame_arm64 to treat INFIX_TYPE_ARRAY explicitly as a pointer passed in a GPR, bypassing the HFA and aggregate logic.
  • Updated generate_forward_argument_moves_arm64 to handle INFIX_TYPE_ARRAY inside the ARG_LOCATION_GPR case by using emit_arm64_mov_reg to copy the pointer from the scratch register (X9) to the argument register.

Full Changelog: https://github.com/sanko/infix/compare/v0.1.2...v0.1.3

sanko/infix v0.1.2 C

[0.1.2] - 2025-11-26

We'll find out where I go from here.

Added

  • Direct Marshalling API: A new, high-performance API (infix_forward_create_direct) for language bindings. This allows the JIT compiler to call user-provided marshaller functions directly, bypassing intermediate argument buffers and reducing overhead.
    • Added infix_forward_create_direct, infix_forward_get_direct_code.
    • Added infix_direct_arg_handler_t and infix_direct_value_t.
    • See #26.
  • Shared Arena Optimization API: Introduced a new set of advanced API functions (infix_registry_create_in_arena, infix_forward_create_in_arena, etc.) that allow the type registry and trampolines to be created within a user-provided, shared memory arena. When objects share an arena, the library avoids deep-copying named type metadata and instead shares pointers to the canonical types, significantly reducing memory consumption and improving trampoline creation performance for applications with many FFI calls referencing a common set of types.
  • Semantic Name Preservation for All Types: The type system can now preserve a semantic name for any type defined in a registry, not just structs and unions. An alias like @MyInt = int32; or @MyHandle = *void; will now produce a type that is structurally identical to its definition but carries the semantic name for introspection.
  • New Introspection API: Added infix_type_get_name(const infix_type* type) to the public API. This function is now the canonical way to retrieve the semantic alias of any type object, if one exists.
  • Added full support for C-style bitfields in structs using the syntax name : type : width (e.g., flags:uint32:3). The layout engine correctly packs them according to System V rules.
  • Added support for C99 Flexible Array Members using the syntax name : [ ? : type ]. The layout engine correctly handles their alignment and placement at the end of structs.

Changed

  • Growable Arena: The internal arena for the type registry is no longer fixed-size. Now, it transparently allocates new memory blocks as needed, removing the risk of allocation failures when registering and/or copying a large number of interconnected types.
  • Type Registry and Printing Logic: The internals of the type registry and the infix_type_print function have been updated to correctly create, copy, and serialize the new name field on infix_type objects, ensuring that semantic aliases are preserved through all API operations and can be correctly round-tripped to strings.
  • Renamed all cookbook examples in /eg/cookbook. I can't expect to keep track of recipe numbers with every little idea I decide to throw into the cookbook so I'll just stop trying to count them.
  • Windows opens the current executable in infix_library_open when the path value is NULL.

Fixed

  • Fixed a series of low-level instruction encoding errors in the AVX and AVX-512 instructions leading to SIGILL errors when calling functions with __m256d or __m512d vector types.
  • Fixed a critical ABI classification bug on SysV where function parameters of an array type (e.g., void func(char s[20])) were incorrectly treated as by-value aggregates. The faulty classification caused infix to generate code that passed the array's content on the stack instead of a pointer, leading to stack corruption, crashes, and incorrect argument marshalling.

Full Changelog: https://github.com/sanko/infix/compare/v0.1.1...v0.1.2

Packed Trampolines
5 min

We have nearly reached the end of the current roadmap. infix is fast, secure, and portable. But optimization is an addiction, and I am already looking at the next bottleneck. The new direct marshalling API wins the benchmark races because it removes the intermediate step of packing C values into a buffer. Double indirection is slower than pulling them straight from the source SV*s.

But what if we start with raw C values? What if we pretend we aren't wrapping a scripting language, but building a high-performance event system or a hot-reloadable game engine?

Currently, you have to do this:

int a = 10;
double b = 20.0;
void* args[] = { &a, &b }; // <--- Indirection #1: The Array
// Inside the JIT:
// 1. Read args[0]  -> get pointer to 'a'
// 2. Read *pointer -> get value 10

This is the libffi model. It's flexible, but it is cache-unfriendly. The args array is in one place, a is on the stack, b might be on the heap. The CPU has to gather scattered memory from a wide range of locations.

I am plotting the implementation of a something that has been on my TODO list almost from the start: packed trampolines. Now that we have a functioning type graph and introspection system, this should be possible.

The Concept: Arguments as a Struct

Instead of passing an array of pointers, what if we passed a single pointer to a block of memory where the arguments are laid out contiguously?

// Concept: The Packed Buffer
struct PackedArgs {
    int a;
    // 4 bytes padding for alignment
    double b;
};

struct PackedArgs args = { 10, 20.0 };
infix_packed_cif(&args);

This changes the JIT's job from "gather" to "offset".

The JIT Transformation

Let's look at the assembly difference on x86-64 (ARM always take a back seat...) for loading the second argument (double b).

Current Approach:

; R14 points to void** args
MOV R15, [R14 + 8] ; Load the pointer to 'b' into scratch reg
MOVSD XMM0, [R15]  ; Dereference to get the value

We have two memory loads. If [R14 + 8] and [R15] are far apart, we might stall on two cache misses.

Proposed Approach:

; R14 points to the packed buffer
MOVSD XMM0, [R14 + 8] ; Load value directly from offset 8

One instruction. One memory load. Perfect spatial locality and that translates into speed.

Implementation Challenges

Implementing infix_forward_create_packed requires a new kind of logic in the ABI layer: layout computation.

The JIT doesn't just need to know that Arg 1 is a double; it needs to know exactly where that double sits in the packed buffer relative to Arg 0.

This implies a pre-calculation phase that mimics our struct layout logic:

// Hypothetical layout logic
size_t current_offset = 0;
for (int i=0; i < num_args; ++i) {
    infix_type* type = arg_types[i];
    // Align the current offset
    size_t align = infix_type_get_alignment(type);
    current_offset = (current_offset + align - 1) & ~(align - 1);
    
    // Store this offset for the JIT emitter
    layout->arg_offsets[i] = current_offset;
    
    current_offset += infix_type_get_size(type);
}

The JIT emitter then uses these pre-calculated offsets to generate the MOV instructions.

Why bother?

For general application logic, saving one MOV instruction is negligible. But for data-oriented design, this is a game changer.

Imagine a system where you have a buffer of network packets or a file loaded from disk. The data is already packed.

  • Current FFI: You must parse the buffer, copy values into local variables, take their addresses, and build a void* array.
  • Packed FFI: You just pass the pointer to the buffer directly to the JIT.

This enables Zero-Copy FFI!

If infix supports this, it becomes more than just a function caller; it becomes a bridge that can map binary data directly to function invocations without deserialization overhead.

The Roadmap

This feature is currently in the design phase. It requires:

  1. Extending the infix_call_frame_layout to support explicit byte offsets for sources.
  2. Adding a infix_packed_layout_create API to helper users build the input buffers correctly.
  3. Updating the architecture emitters to support "Base + Offset" loading (which they mostly do already).

It’s the logical conclusion of the project: moving from "calling functions dynamically" to "executing data."

This is not a top priority but stay tuned.

Language Specific Trampolines
5 min 1

This is an explanation and expansion of discussion #26 now that I've started actually implemented and designed my idea.

Overview

The Direct Marshalling API (also known as the "Bundle" API) is an advanced feature designed specifically for high-performance language bindings (e.g., Perl, Python, Ruby, Lua). I've found arbitrary benchmarks of Affix.pm shrinks runtime by ~15% on SysV and nearly 25% on Windows over infix's standard trampolines.

The Problem with Standard FFI

In a standard infix_forward_create call, the workflow at runtime looks like this:

  1. Host Language: Unboxes values from native objects (e.g., PyObject*) into temporary C variables.
  2. Host Language: Constructs an array of pointers (void* args[]) pointing to those temporaries.
  3. Infix JIT: Reads from args[], moves data into registers/stack.
  4. Target: Function executes.

Steps 1 and 2 often require malloc/free or complex stack management in the host language loop, creating overhead.

The Direct Solution

The Direct Marshalling API moves the unboxing logic inside the JIT-compiled trampoline.

  1. Host Language: Passes an array of raw object pointers (e.g., PyObject**) directly to the trampoline.
  2. Infix JIT: Calls specific, user-provided "Marshaller" functions to unbox data just-in-time into the correct registers.
  3. Target: Function executes.

This eliminates the intermediate void* array and temporary C variables managed by the caller, significantly reducing cache pressure and instructions per call.

API Reference

Data Structures

infix_direct_value_t

A union returned by scalar marshallers. Since a C function can only return one value, this union allows the marshaller to return any primitive type, which the JIT then moves to the correct register class (Integer vs XMM).

typedef union {
    uint64_t u64;  // For all unsigned integers <= 64 bits
    int64_t i64;   // For all signed integers <= 64 bits
    double f64;    // For float and double
    void* ptr;     // For pointers
} infix_direct_value_t;

infix_direct_arg_handler_t

A struct containing function pointers that define how to handle a specific argument. You must provide one of these for every argument in the signature.

typedef struct {
    // 1. Scalar Marshaller: Called for primitives and simple pointers.
    //    Input:  Pointer to your language object (e.g., PyObject*).
    //    Output: The raw C value in the union.
    infix_direct_value_t (*scalar_marshaller)(void* source_object);

    // 2. Aggregate Marshaller: Called for structs/unions passed by value.
    //    Input:  Pointer to language object.
    //    Input:  Pointer to destination buffer (stack-allocated by JIT).
    //    Input:  Type info for introspection.
    void (*aggregate_marshaller)(void* source_object, void* dest_buffer, const infix_type* type);

    // 3. Writeback Handler: Called after the function returns (for out-params).
    //    Input:  Pointer to language object.
    //    Input:  Pointer to the (potentially modified) C data.
    //    Input:  Type info.
    void (*writeback_handler)(void* source_object, void* c_data_ptr, const infix_type* type);
} infix_direct_arg_handler_t;

Functions

infix_forward_create_direct

Generates the specialized trampoline.

infix_status infix_forward_create_direct(
    infix_forward_t** out_trampoline,
    const char* signature,
    void* target_function,
    infix_direct_arg_handler_t* handlers, // Array of handlers, one per argument
    infix_registry_t* registry
);

infix_forward_get_direct_code

Retrieves the callable function pointer. Note the signature differs from standard trampolines.

// lang_objects is an array of pointers to your language objects (e.g. PyObject* args[])
typedef void (*infix_direct_cif_func)(void* return_buffer, void** lang_objects);

infix_direct_cif_func cif = infix_forward_get_direct_code(trampoline);

Usage Example

Imagine creating a binding for a scripting language where every value is a Value struct. We want to call void move_point(Point p, int dx).

1. Define the Source Object

typedef enum { VAL_INT, VAL_OBJECT } ValType;
typedef struct {
    ValType type;
    union { int i; void* fields[]; } data;
} Value;

2. Implement Marshallers

// Marshaller for 'int' arguments
infix_direct_value_t marshal_int(void* obj_ptr) {
    Value* v = (Value*)obj_ptr;
    return (infix_direct_value_t){ .i64 = (v->type == VAL_INT ? v->data.i : 0) };
}

// Marshaller for 'Point' struct arguments
void marshal_point(void* obj_ptr, void* dest, const infix_type* type) {
    Value* v = (Value*)obj_ptr;
    Point* p = (Point*)dest;
    // Assume we know the layout of the Value's fields for this example
    Value* val_x = (Value*)v->data.fields[0];
    Value* val_y = (Value*)v->data.fields[1];
    p->x = (double)val_x->data.i;
    p->y = (double)val_y->data.i;
}

3. Create the Trampoline

// Signature: Point move_point(Point p, int dx);
// Arg 0: Point (Aggregate)
// Arg 1: int (Scalar)

infix_direct_arg_handler_t handlers[2] = {0};

// Setup Arg 0 (Point)
handlers[0].aggregate_marshaller = marshal_point;

// Setup Arg 1 (int)
handlers[1].scalar_marshaller = marshal_int;

infix_forward_t* trampoline;
infix_forward_create_direct(&trampoline,
    "({double,double}, int) -> void",
    (void*)move_point,
    handlers,
    NULL);

4. Call It

// Prepare array of pointers to Value objects
Value* arg0 = ...; // Point object
Value* arg1 = ...; // Int object
void* script_args[] = { arg0, arg1 };

// Call
infix_direct_cif_func cif = infix_forward_get_direct_code(trampoline);
cif(NULL, script_args); // No return buffer needed for void

Performance Considerations

  1. Register Pressure: The generated JIT code must shuffle data between the lang_objects array, the marshaller return registers, and the argument registers. infix optimizes this, but complex signatures with many arguments may spill to the stack.
  2. Inlining: The marshaller functions you provide are called via function pointers. For maximum performance, keep them small and fast.
  3. Writebacks: Using writeback_handler adds overhead (allocating stack space, preserving return registers, making the call). Use only when necessary (pointers/references).
sanko/infix Polish C

Really sanding down the rough edges this time around. This release includes significant ergonomic improvements to the high-level API.

Added

  • New Signature Keywords: Added keywords for common C and C++ types to improve signature readability and portability.
    • Added size_t and ssize_t as platform-dependent abstract types.
    • Added char8_t, char16_t, and char32_t as aliases for uint8, uint16, and uint32 for better C++ interoperability.
  • Cookbook Examples: Extracted all recipes from the cookbook documentation into a comprehensive suite of standalone, compilable example programs located in the eg/cookbook/ directory.
  • Advanced C++ Recipes: Added new, advanced cookbook recipes demonstrating direct, wrapper-free interoperability with core C++ features:
    • Calling C++ virtual functions by emulating v-table dispatch.
    • Bridging C-side stateful callbacks with C++ objects that expect std::function or similar callable objects.

Changed

  • Improved C++ Interoperability Recipes: Refined the C++ recipes to focus on direct interaction with C++ ABIs (mangled names, v-tables) rather than relying on C-style wrappers, showcasing more advanced use cases.

  • Improved wchar_t Guidance: Added a dedicated cookbook recipe explaining the best-practice for handling wchar_t and other semantic string types via the Type Registry, ensuring signatures are unambiguous and introspectable.

  • Enhanced High-Level API for Registered Types. The primary creation functions (infix_forward_create, infix_forward_create_unbound, infix_reverse_create_callback, and infix_reverse_create_closure) can now directly accept a registered named type as a signature.

    // The high-level API now understands the "@Name" syntax directly.
    // Assume the registry already has "@Adder_add_fn = (*{ val: int }, int) -> int;"
    infix_reverse_create_callback(&ctx, "@Adder_add_fn", (void*)Adder_add, reg);
    
  • The infix_read_global and infix_write_global functions now take an additional infix_registry_t* argument to support reading and writing global variables that are defined by a named type (e.g., @MyStruct).

Fixed

  • Fixed a critical parsing bug in infix_register_types that occurred when defining a function pointer type alias (e.g., @MyFunc = (...) -> ...;). The preliminary parser for finding definition boundaries would incorrectly interpret the > in the -> token as a closing delimiter, corrupting its internal nesting level calculation. This resulted in an INFIX_CODE_UNEXPECTED_TOKEN error and prevented the registration of function pointer types. The parser is now context-aware and correctly handles the -> token, allowing for the clean and correct registration of function pointer aliases as intended.

Full Changelog: https://github.com/sanko/infix/compare/v0.1.0...v0.1.1

System calls via FFI
7 min

I've had this idea for a while now and might put effort into it next. I understand that this would be a massive task but the first 30% of it is already accomplished by infix existing in its current state.

Here's my proposal:

The design of this will be based on the same principles as the rest of infix: one-time setup for performance, declarative signature string, and clean separation between the generic, user-facing API and the messy platform-specific backends.

The Core API: A Familiar, Handle-Based Pattern

Instead of a single, syscall() function, we would mirror the trampoline pattern by creating a handle. This allows infix to perform the expensive lookup and JIT-compilation once.

/**
 * @brief An opaque handle to a JIT-compiled system call stub.
 */
typedef struct infix_syscall_t infix_syscall_t;

/**
 * @brief Creates a handle to a specific system call.
 *
 * This function looks up the syscall by its platform-specific name, parses the
 * signature, and generates a JIT stub to invoke it. Like a bound trampoline.
 *
 * @param[out] out_handle Receives the created handle.
 * @param[in] syscall_specifier A string identifying the syscall, using a
 *            platform namespace (e.g., "linux:write", "win:NtCreateFile").
 * @param[in] signature The infix signature of the syscall's arguments and return type.
 * @param[in] registry An optional registry for any named types used in the signature.
 * @return INFIX_SUCCESS on success.
 */
infix_status infix_syscall_create(
    infix_syscall_t ** out_handle,
    const char * syscall_specifier,
    const char * signature,
    infix_registry_t * registry
);

/**
 * @brief Invokes the system call.
 *
 * This function is the syscall version of of `infix_cif_func`. It executes
 * the JIT-compiled stub.
 *
 * @param handle The handle created by infix_syscall_create.
 * @param out_return_value A pointer to a buffer to receive the raw return value
 *        (e.g., an intptr_t). This value may indicate an error, which must be
 *        interpreted according to the OS's conventions (e.g., negative value for errno).
 * @param args An array of pointers to the argument values.
 */
void infix_syscall_call( infix_syscall_t * handle, void * out_return_value, void ** args );

/**
 * @brief Destroys a syscall handle and frees its resources.
 */
void infix_syscall_destroy( infix_syscall_t * handle );

A Portability 'Solution': The "Syscall Specifier"

The biggest challenge is naming. write means different things on different OSes so my proposed solution is a namespaced string: "os_name::syscall_name".

  • "linux::write": Refers to the write syscall on any Linux system.
  • "win::NtWriteFile": Refers to the NtWriteFile syscall on Windows.
  • "freebsd::write": Refers to the write syscall on FreeBSD.
  • "darwin::write": Refers to the write syscall on macOS/Darwin.

This approach acknowledges that syscalls are not truly portable at a semantic level and requires the developer to know their target OS. I might even need to add a version number to this somehow. Either way, this naming system makes the backend implementation clean and manageable, as each namespace can have its own simple name-to-number lookup table.

Example Usage: Writing to stdout

Here's how you'd call write(1, "hello\n", 6) on Linux:

#include <infix/infix.h>
#include <stdio.h>
#include <unistd.h> // For ssize_t

void portable_syscall_example_linux() {
    infix_syscall_t * handle = NULL;
    const char* specifier = "linux::write";
    // The signature for ssize_t write(int fd, const void *buf, size_t count);
    const char * signature = "(int, *void, uint64) -> sint64"; // Same signature language as infix...

    // Create the handle once.
    infix_status status = infix_syscall_create(&handle, specifier, signature, NULL);
    if (status != INFIX_SUCCESS)
        // Handle error...
        return;
  
    // Prepare arguments for a specific call.
    int fd = 1; // stdout
    const char * message = "Hello from a direct syscall!\n";
    uint64_t count = 29;
    void * args[] = { &fd, &message, &count };
    int64_t return_value;

    // Call the syscall. This is the fast part.
    infix_syscall_call( handle, &return_value, args );

    if (return_value < 0)
        printf("Syscall failed with errno: %lld\n", (long long) 0-return_value);
    else 
        printf("Syscall returned %lld (bytes written).\n", (long long)return_value);
    
    infix_syscall_destroy(handle);
}

Internals

I'll probably add a new v-table specific to system calls. Probably something like this in infix_internals.h:

typedef struct {
    // Looks up a name like "write" and returns its number for this OS.
    long (*lookup_syscall_number)(const char * name);
    // The ABI for syscalls (which registers to use for number and args).
    // This could even reuse parts of the main ABI spec.
    infix_abi_spec * syscall_abi;
    // The specific instruction to emit (e.g., the bytes for `syscall` or `svc`).
    void (* generate_syscall_instruction)(code_buffer * buf);
} infix_syscall_spec;

When infix_syscall_create is called (assume linux)...

  • ...it parses the specifier (e.g., "linux::write").

  • ...it selects the correct infix_syscall_spec for "linux".

  • ...it calls spec->lookup_syscall_number("write") to get the number (e.g., 1).

  • ...it generates a trampoline that:

    • Moves the syscall number 1 into the correct register (e.g., rax).
    • Moves the user's arguments (fd, message, count) into the correct argument registers (rdi, rsi, rdx).
    • Calls spec->generate_syscall_instruction() to emit the syscall instruction.
    • Handles the return value from rax.

Before I write any actual code, I've tried to piece together a few solid resources for finding the system call tables and conventions for each major supported platform.


Linux

Linux has a stable and well-documented syscall ABI for each architecture. The syscall number is placed in a specific register (rax on x86-64, x8 on AArch64), and then the syscall (or svc) instruction is executed.

Sources:

  • https://filippo.io/linux-syscall-table/ is a very easy-to-use reference. It has as a dropdown to switch between architectures (x86-64, aarch64, etc.).
  • https://syscalls64.paolostivanin.com/ also includes information on the registers used for each argument.
  • syscalls(2)/man syscall/man syscalls man pages provide a complete list of all system calls and some examples.
  • The kernel's unistd.h header for each architecture would be the definitive list to go by but might be a little dense.

Windows (Native API)

Windows does not have a stable, numbered syscall interface in the same way as Linux. Instead, the lowest-level supported interface is the Native API, which consists of functions exported by ntdll.dll (e.g., NtCreateFile, NtWriteFile). While these functions ultimately use a syscall or sysenter instruction, the numbers can change between Windows versions and even service packs. I'll need to enhance the OS detection in infix_config.h considerably. The only 'stable' interface is the function in ntdll.dll.

Sources:


macOS (XNU Kernel)

macOS uses a combination of POSIX-style syscalls and lower-level "Mach traps". It's based on FreeBSD and the numbers are defined in the kernel source code which is open source: https://github.com/apple-oss-distributions/xnu.


FreeBSD

FreeBSD also uses a central file in the kernel source to define its syscalls: https://cgit.freebsd.org/src/tree/sys/kern/syscalls.master

Coercion of arguments into a solid block of memory
2 min 1

Shower thought time... Internally, would it be faster to coerce args that would end up on the stack into a single block of malloc'd memory? Currently, our trampolines must perform a double-indirection to put data on the stack:

  1. load the pointer (something like mov r15, [r14 + i*8]) to use a scratch register.
  2. dereference that pointer to get the actual data (mov rax, [r15])
  3. move the actual data to the destination register/stack slot (mov rdi, rax)

With cache misses, longer arg lists (> 16?), etc. this is a choke point for speed (honestly, microseconds with millions of calls). Alternatively, if we let actual compiler designers deal with those and just allocate a single block of data that'll end up on the stack with something like sub rsp, #size, loop through all N args and call something like memcpy(block + offset, args[N], size); which would be more work being done in C but...

I'll have to benchmark moving our current data gathering step from the JIT path vs. having it done by actual compiler designers... For a 'typical' system this would be faster in theory but would it be faster enough to make it worth the work breaking all JIT generation code...

Todo list
1 min

Features I'd love to include when I have the time will find their way here.

This is not a replacement for the issue tracker.