Sometimes, the best tool for the job involves three different tools. You might have legacy math models in Fortran, performance-critical loops in Assembly, and a clean C API to orchestrate them.
By mastering the Polyglot strategy, you're no longer forced to rewrite battle-tested legacy code or compromise on performance. You can let Fortran handle the numerics, Assembly handle the vectorization, and C handle do coordination, all while driving the whole machine from Perl. Affix::Build turns what used to be a nightmare of Makefiles and linker flags into a simple script, freeing you to choose the absolute best tool for every specific problem.
The Recipe
Here, we'll create a library called number_cruncher. It uses C as the API controller, Fortran for the calculation logic, and Assembly for a low-level incrementer. It's not a very useful example but it's a good demonstration of our task.
use v5.40;
use Affix qw[:all];
use Affix::Build;
use Config;
# 1. Initialize
# We assume we are compiling different languages into one binary
my $c = Affix::Build->new( name => 'number_cruncher' );
# 2. C Source (The Orchestrator)
# Standard C function that will be the entry point or helper
$c->add( \<<~'', lang => 'c' );
#include <stdio.h>
#ifdef _WIN32
__declspec(dllexport)
#endif
int core_version() { return 1; }
# 3. Fortran Source (The Math Engine)
# Uses ISO C Binding to export 'fortran_add' as a standard C symbol
$c->add( \<<~'', lang => 'f90' );
function fortran_add(a, b) bind(c, name='fortran_add')
use iso_c_binding
integer(c_int), value :: a, b
integer(c_int) :: fortran_add
fortran_add = a + b
end function
# 4. Assembly Source (The Optimizer)
# A simple function 'asm_inc(x)' that returns x + 1.
# We must detect the CPU architecture to provide the correct instructions.
my $arm = $Config{archname} =~ /arm64|aarch64/;
$c->add( \( $arm ? <<~'' : $^O eq 'MSWin32' ? <<~'': <<~'' ), lang => $arm ? 's' : 'asm' );
; ARM64: Linux, macOS, Windows ARM uses standard system assembler (.s)
.global asm_inc
.text
.align 2
asm_inc:
add w0, w0, #1 ; Increment w0 (first arg) by 1
ret
; Win64 x86_64: Uses RCX for first argument
global asm_inc
section .text
asm_inc:
mov eax, ecx
inc eax
ret
; SysV x86_64: Uses RDI for first argument
global asm_inc
section .text
asm_inc:
mov eax, edi
inc eax
ret
# 5. Build
# The compiler detects mixed languages and switches to 'Polyglot Mode'
my $lib = $c->link;
say 'Compiled Polyglot Library: ' . $lib;
# 6. Bind
affix $lib, 'core_version', [] => Int;
affix $lib, 'fortran_add', [ Int, Int ] => Int;
affix $lib, 'asm_inc', [Int] => Int;
# 7. Run
say 'Core Version: ' . core_version();
say 'Fortran Add: ' . fortran_add( 10, 20 );
say 'ASM Inc: ' . asm_inc(99);
Output:
Compiled Polyglot Library: C:/Users/S/AppData/Local/Temp/Fye21DPXMw/number_cruncher.dll.1
Core Version: 1
Fortran Add: 30
ASM Inc: 100
How It Works
-
The Polyglot Strategy
When
Affix::Builddetects multiple languages, it switches from 'Native' mode (one compiler does everything) to 'Polyglot' mode.- It asks
gccto compile the C code tomath_core.o. - It asks
gfortranto compile the Fortran code tomath_algos.o. - It asks
nasm(or the system assembler on ARM) to assemble the ASM code tofast.o. - It invokes the system linker (usually via
ccorc++) to combine all three objects into the final shared library.
- It asks
Kitchen Reminders
-
ABI Compatibility
The reason this works is that C, Fortran, and Assembly all speak the same "C ABI" (Application Binary Interface). Rust, Zig, and Odin also support this. Languages like Go and C# require heavier runtimes, but
Affix::Buildhandles 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_cppvor 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.