[v0.0.3] - 2026-02-17
Changed
- Adding an optional timeout to
await_readandawait_write. - Allow fibers to return complex data (AV*, HV*).
Full Changelog: https://github.com/sanko/Acme-Parataxis.pm/compare/v0.0.2...v0.0.3
Human.
await_read and await_write.Full Changelog: https://github.com/sanko/Acme-Parataxis.pm/compare/v0.0.2...v0.0.3
coro_yield by adding NULL checks for destroyed or missing fibers.last_sender tracking to prevent parent_id cycles.Full Changelog: https://github.com/sanko/Acme-Parataxis.pm/compare/v0.0.1...v0.0.2
I really just wanna see how CPAN smokers deal with this.
Full Changelog: https://github.com/sanko/Acme-Parataxis.pm/commits/v0.0.1
Valgrind directed the work in Affix itself but infix got a lot of platform stability fixes which found their way into Affix by way of new Float16 support, bitfield width support, and SIMD improvements.
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.dc cvau, ic ivau, etc.), ensuring generated code is immediately visible to the CPU..eh_frame generation for ARM64 Linux FORWARD trampolines, correcting the instruction sequence and offsets to enable reliable C++ exception propagation.vzeroupper calls in epilogues when AVX instructions are potentially used, avoiding transition penalties._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.Float16 support
Added the Float16 keyword to Affix.pm.
Implemented float_to_half and half_to_float conversion logic in Affix.c (IEEE 754).
Added optimized opcodes (OP_PUSH_FLOAT16, OP_RET_FLOAT16) to the internal VM dispatcher for high-performance marshalling.
Bitfield Support:
Enhanced Struct [...] syntax in Affix.pm to support bitfield widths (e.g., a => UInt32, 3).
Implemented bitmask-based marshalling in Affix.c to correctly pack and unpack C-style bitfields within structs.
SIMD Vector Improvements:
Added M512, M512d, and M512i type helpers.
Ensured compatibility with infix's refined vector alignment and passing rules.
[infix] Added support for half-precision floating-point (float16).
[infix] Implemented C++ exception propagation through JIT frames on Linux (x86-64 and ARM64) using manual DWARF .eh_frame generation and __register_frame.
[infix] Implemented Structured Exception Handling (SEH) for Windows x64 and ARM64 for C++ exception propagation through trampolines.
[infix] 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).
[infix] Added support for 256-bit (AVX) and 512-bit (AVX-512) vectors in the System V ABI.
[infix] Added support for receiving bitfield structs in reverse call trampolines.
[infix] 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.
[infix] 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.
infix_type_create_vector to use the vector's full size for its natural alignment (e.g., 32-byte alignment for __m256).XMM0._infix_registry_entry_t. Lookups and rehashing now use this stored hash, significantly reducing string hashing overhead during type resolution and registry scaling.Full Changelog: https://github.com/sanko/Affix.pm/compare/v1.0.6...v1.0.7
TARGET_DIRECT_TRAMPOLINE_GENERATOR to the regression tester in t/850_regression_cases.c to support debugging and preventing regressions in the direct marshalling pipeline._infix_forward_create_direct_impl where the ref_count of the newly created trampoline handle was not being initialized. This caused the handle to never be freed during destruction as the library incorrectly assumed other references existed.Full Changelog: https://github.com/sanko/infix/compare/v0.1.5...v0.1.6
Net::BitTorrent::Storage where _evict_one incorrectly deleted entire file caches and _flush_one failed to remove data from cache after writing to disk.Full Changelog: https://github.com/sanko/Net-BitTorrent.pm/compare/v2.0.0...v2.0.1
After like 16 years, I've rewritten Net::BitTorrent from scratch. This is it.
Full Changelog: https://github.com/sanko/Net-BitTorrent.pm/commits/v2.0.0
This release expands platform stability. Focus was on SEH and DWARF unwinding, float16 and AVX-512 vector types.
float16)..eh_frame generation and __register_frame.infix_forward_create_safe API to establish an exception boundary that catches native exceptions and returns a dedicated error code (INFIX_CODE_NATIVE_EXCEPTION).--sanity) that emits extra JIT instructions to verify stack pointer consistency around user-provided marshaller calls, making it easier to debug corrupting language bindings.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.INFIX_* namespace (e.g., INFIX_OS_LINUX, INFIX_ARCH_X64, INFIX_COMPILER_GCC) throughout the codebase.infix_executable_make_executable and internal layout structures to track trampoline prologue sizes, necessary for SEH and DWARF registration.infix_type_create_vector to use the vector's full size for its natural alignment (e.g., 32-byte alignment for __m256).XMM0._infix_registry_entry_t. Lookups and rehashing now use this stored hash, significantly reducing string hashing overhead during type resolution and registry scaling.S_, S0_, etc.) for complex or repeated types.0-9) for parameter lists.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.dc cvau, ic ivau, etc.), ensuring generated code is immediately visible to the CPU..eh_frame generation for ARM64 Linux FORWARD trampolines, correcting the instruction sequence and offsets to enable reliable C++ exception propagation.vzeroupper calls in epilogues when AVX instructions are potentially used, avoiding transition penalties._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.Full Changelog: https://github.com/sanko/infix/compare/v0.1.4...v0.1.5
Full Changelog: https://github.com/sanko/Net-uTP.pm/compare/v1.0.0...v1.0.1
Full Changelog: https://github.com/sanko/Acme-Bitfield.pm/commits/v1.0.0
Full Changelog: https://github.com/sanko/Net-uTP.pm/commits/v1.0.0
Full Changelog: https://github.com/sanko/Digest-Merkle-SHA256.pm/commits/v1.0.0
Hello, world!
Full Changelog: https://github.com/sanko/At.pm/compare/1.0...1.1
Full Changelog: https://github.com/sanko/At.pm/commits/1.0
Most of this version's work went into threading stability, ABI correctness, and security within the JIT engine.
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_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.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.Pointer[SV] types were incorrectly treated as generic pointers if typedef'd. They are now correctly unwrapped into Perl CODE refs or blessed objects.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.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.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)).__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.h header included an internal file (common/compat_c23.h). The header is now fully self-contained and defines INFIX_NODISCARD for attribute compatibility.MOVSD) for all SSE arguments, corrupting the upper half of vector arguments. They now correctly use MOVUPS.STR Qn) instead of falling back to 64-bit/32-bit stores or GPRs.INTEGER class, causing the trampoline to look in RDI/RSI instead of XMM registers.FlushInstructionCache after JIT compilation.infix_type_create_packed_struct to 1MB to prevent integer wrap-around bugs in layout calculation.LDRSW. Implemented LDRSH and LDRSB.MAP_JIT when the com.apple.security.cs.allow-jit entitlement is detected.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
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.
infix_get_version and infix_version_t to the public API. This allows applications to query the semantic version of the library at runtime.infix_registry_clone to support deep-copying type registries for thread-safe interpreter cloning.t/404_simd_vectors.c: new unit tests for 128-bit SIMD vectors.
INFIX_API and INFIX_INTERNAL macros to explicitly control symbol visibility.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_*) 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.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.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)).__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.build.pl) to better handle external tool failures. Coverage gathering commands (like codecov) are now allowed to fail gracefully without breaking the build pipeline.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.MOVSD) for all SSE arguments, corrupting the upper half of vector arguments. They now correctly use MOVUPS.STR Qn) instead of falling back to 64-bit/32-bit stores or GPRs.INTEGER class, causing the trampoline to look in RDI/RSI instead of XMM registers.FlushInstructionCache after JIT compilation.infix_type_create_packed_struct to 1MB to prevent integer wrap-around bugs in layout calculation.LDRSW. Implemented LDRSH and LDRSB.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.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.fuzz/fuzz_helpers.c by adding explicit braces.infix_internals.h by removing the obsolete declaration for _infix_forward_create_internal._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.INFIX_DIALECT_ITANIUM_MANGLING) and MSVC (INFIX_DIALECT_MSVC_MANGLING) mangling in signature stringification. This is mostly for debugging the type system quickly.
@Outer::Inner::MyClass).U for structs, T for unions).MAP_JIT when the com.apple.security.cs.allow-jit entitlement is detected.pthread_jit_write_protect_np to maintain W^X compliance.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.
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.
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
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).
RDI, RSI, RDX...RAXRBX, 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.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
%reg, src, dest).reg, dest, src) via NASM.
If you are targeting Windows and Intel, you should use lang => 'asm' (NASM) instead of 's'.If you are doing heavy scientific computing, you will eventually encounter Fortran (BLAS, LAPACK).
Binding Fortran can be tricky because:
DGEMM becomes dgemm_).However, modern Fortran (2003+) offers the iso_c_binding module, which allows us to define a standard C ABI.
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) );
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.
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.
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.
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');
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.
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.
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.
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:
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};
$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.
xmake works on Windows, Linux, and macOS.conan, vcpkg, brew, and its own repository..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.
Full Changelog: https://github.com/sanko/Alien-Xmake/compare/0.07...0.08
Allow users to define types in Affix::Wrap
Full Changelog: https://github.com/sanko/Affix.pm/compare/v1.0.4...v1.0.5
Minor documentation tweaks
Full Changelog: https://github.com/sanko/Affix.pm/compare/v1.0.3...v1.0.4
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.
Initialize your project:
minil new Acme-Image-Stb
cd Acme-Image-Stb
mkdir builder
minil.toml)We need to tell Minilla two things:
Module::Build (instead of Module::Build::Tiny).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"
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;
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';
};
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
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
By subclassing Module::Build, we have seamlessly integrated Affix::Build into the standard Perl toolchain.
cpanm won't know you aren't using XS. It just works.blib/arch/auto, we respect Perl's directory structure for binary binaries. This demo does it in a rather unsophisticated way...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.
Based on infix v0.1.3
printf).variadic_cache to cache trampolines for repeated calls, ensuring high performance.sint64, floats to double, and strings to *char.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.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.File and PerlIO types.
PerlIO* => Pointer[PerlIO]).FILE* from C and using them as standard Perl filehandles (FILE* => Pointer[File]).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::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.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).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.Array[Char/UChar]. Reading these arrays now respects the explicit length rather than stopping at the first null byte.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.Full Changelog: https://github.com/sanko/Affix.pm/compare/v1.0.2...v1.0.3
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.
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:
...into this:
Simple enough.
This recipe demonstrates two advanced techniques:
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;
}
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.
Full Changelog: https://github.com/sanko/Alien-Xmake/compare/0.05...0.06
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.
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
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.
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)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:
PDL (Perl Data Language) The standard for high-performance numerical computing in Perl. It is heavily optimized for processing large arrays ("piddles") in C.
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.
This set of benchmarks tests using 128-bit SIMD vectors (adding four floats at once):
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 ) }
}
);
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% --
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.
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.
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 );
}
}
);
Verifying results...
Verification Successful: Matches at [500, 500] (55)
Matrix Benchmark (1000x1000)
Rate PDL Affix
PDL 78.5/s -- -99%
Affix 6795/s 8555% --
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.
affix), versus lines of XS macros or complex PDL threading logic.Of course, none of these points matter if you can't write the C or Fortran to emulate the features of PDL.
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.
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!
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:
$this to get $vtable_addr.$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.
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.
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.
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.
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
Droid_new allocates memory on the C++ heap (new Droid).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.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.
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.
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;
affix_type
This method returns the string representation of the binding.
User: Struct[ id => Int, name => Array[Char, 32] ].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!
.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.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.
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
->wrap($lib)
This method iterates over every node found in the AST.
Affix::typedef, registering types like Vector3 so they can be used in signatures.Affix::pin, tying the Perl variable $GRAVITY to the symbol in the shared library.Affix::affix, creating the Perl subroutine vec_add.Note that we didn't write a single Struct[...] or affix signature manually.
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(...);
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.
Affix::Wrap uses a dual-driver approach to read C code:
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.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
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.
<stdio.h> or "my_types.h"), pass their locations to the constructor via include_dirs => ['/path/to/includes'].Affix::Wrap also captures comments! In the next chapters, we will use this to generate documentation automatically.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.
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
The Polyglot Strategy
When Affix::Build detects multiple languages, it switches from 'Native' mode (one compiler does everything) to 'Polyglot' mode.
gcc to compile the C code to math_core.o.gfortran to compile the Fortran code to math_algos.o.nasm (or the system assembler on ARM) to assemble the ASM code to fast.o.cc or c++) to combine all three objects into the final shared library.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.
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.
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 );
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 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 );
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 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 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 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 );
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.
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.
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
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:
.dll extension and creates a PE binary..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.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:
.o or .a) using the appropriate compiler for each language (rustc for Rust, cc for C, etc.).# 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.
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.
"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:
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).
The current dominant FFI module on CPAN. It uses libffi (a portable, general-purpose Foreign Function Interface library) to handle argument shuffling.
Our pride and joy. Wraps our custom JIT engine: infix.
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 ) }
}
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% --
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.
"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.
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% --
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.
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.
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.
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' );
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:
init and process subs.3. Execution
When C calls p->process(data), it hits the trampoline which bounces everything over to your Perl sub.
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.
Manual free() is error-prone. We can use Perl's DESTROY phase to automate it.
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.
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).
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.
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.
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!
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();
}
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
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).
System calls fail. In C, you check the global errno variable (or GetLastError() on Windows). In Affix, we expose this via errno().
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;
}
errno returns a dualvar
2 (ENOENT)This makes logging errors rather convenient.
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.
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`;
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'; <> };
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;
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;
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) |
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*.
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 ) }
}
);
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.
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.
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.
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.
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)"
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:
This allows you to write readable code (eq 'STATE_...') while maintaining high performance for numeric comparisons.
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).
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.
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
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.
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.
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
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.
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).
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.
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
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:
SV*).$compare_fn.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.
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!
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.
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;
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.
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.
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.
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
1. In-Place Modification
When you pass a Perl ArrayRef to an Array argument:
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.
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.
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.
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
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.
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.
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.
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.
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();
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.
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.
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.
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);
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!
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.
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.
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);
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.
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).
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).
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;
1. The libc Helper
Finding the standard library is platform-dependent:
libc.so.6 (usually)libSystem.B.dylibmsvcrt.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.
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.
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.
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);
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.
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.
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.
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]);
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.
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.
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.
This recipe displays a native Windows message box containing Unicode characters (an emoji). It looks like this:
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 );
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:
\0\0).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.
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.
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.
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]";
This creates a seamless bridge between the two languages. Here is what is happening under the hood:
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.
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.
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.
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.
Because we defined the types as Pointers, we must pass B
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!
The chef would like to remind you of things you should have noted or learned from this recipe.
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.
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 );
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.
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.
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).
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?)
This release contains real-world usage fixes since I'm using it in Affix.pm and not just experimenting with different JIT forms.
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.@Node;) did not create valid placeholder types, causing subsequent references (e.g., *@Node) to fail resolution with INFIX_CODE_UNRESOLVED_NAMED_TYPE.infix_registry_print to explicitly include forward declarations in the output, improving introspection visibility.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.prepare_forward_call_frame_arm64 to treat INFIX_TYPE_ARRAY explicitly as a pointer passed in a GPR, bypassing the HFA and aggregate logic.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
This is the first public release of SDL3.pm, a pure Perl wrapper for SDL3.
https://github.com/Perl-SDL3/SDL3.pm/compare/v0.0.1...v0.0.2
Full Changelog: https://github.com/sanko/Affix.pm/compare/v0.12.0...v1.0.0
We'll find out where I go from here.
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.
infix_forward_create_direct, infix_forward_get_direct_code.infix_direct_arg_handler_t and infix_direct_value_t.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.@MyInt = int32; or @MyHandle = *void; will now produce a type that is structurally identical to its definition but carries the semantic name for introspection.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.name : type : width (e.g., flags:uint32:3). The layout engine correctly packs them according to System V rules.name : [ ? : type ]. The layout engine correctly handles their alignment and placement at the end of structs.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./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.infix_library_open when the path value is NULL.SIGILL errors when calling functions with __m256d or __m512d vector types.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
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.
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".
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.
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.
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.
void* array.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.
This feature is currently in the design phase. It requires:
infix_call_frame_layout to support explicit byte offsets for sources.infix_packed_layout_create API to helper users build the input buffers correctly.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.
This is an explanation and expansion of discussion #26 now that I've started actually implemented and designed my idea.
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.
In a standard infix_forward_create call, the workflow at runtime looks like this:
PyObject*) into temporary C variables.void* args[]) pointing to those temporaries.args[], moves data into registers/stack.Steps 1 and 2 often require malloc/free or complex stack management in the host language loop, creating overhead.
The Direct Marshalling API moves the unboxing logic inside the JIT-compiled trampoline.
PyObject**) directly to the trampoline.This eliminates the intermediate void* array and temporary C variables managed by the caller, significantly reducing cache pressure and instructions per call.
infix_direct_value_tA 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_tA 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;
infix_forward_create_directGenerates 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_codeRetrieves 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);
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).
typedef enum { VAL_INT, VAL_OBJECT } ValType;
typedef struct {
ValType type;
union { int i; void* fields[]; } data;
} Value;
// 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;
}
// 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);
// 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
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.writeback_handler adds overhead (allocating stack space, preserving return registers, making the call). Use only when necessary (pointers/references).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.
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.
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 );
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.
stdoutHere'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);
}
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:
1 into the correct register (e.g., rax).fd, message, count) into the correct argument registers (rdi, rsi, rdx).spec->generate_syscall_instruction() to emit the syscall instruction.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 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:
syscalls(2)/man syscall/man syscalls man pages provide a complete list of all system calls and some examples.unistd.h header for each architecture would be the definitive list to go by but might be a little dense.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:
ntdll.dll functions.ntdll.dll.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.
syscalls.master file within the XNU kernel source code and can be found here: https://github.com/apple-oss-distributions/xnu/blob/main/bsd/kern/syscalls.masterFreeBSD also uses a central file in the kernel source to define its syscalls: https://cgit.freebsd.org/src/tree/sys/kern/syscalls.master
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:
mov r15, [r14 + i*8]) to use a scratch register.mov rax, [r15])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...
Neat.