Bridging I/O between languages is historically one of the most fragile aspects of FFI. While handling file descriptors manually as opaque pointers may be effective on POSIX systems, that approach is a minefield on Windows. Affix solves this by introducing two smart types: File and PerlIO. These types handle the extraction, translation, and lifecycle management of file handles automatically.
This chapter covers the correct usage patterns and advanced scenarios like multiplexing I/O between Perl and C on the same handle.
The Basic Recipe: Writing from C
Let's start with the basics: passing a Perl filehandle to a C function that expects FILE*.
Note the use of Pointer[File] in the signature; using just File would attempt to pass the opaque structure by value, which is not what we want.
use v5.40;
use Affix;
use Affix::Build;
#
# 1. Build the lib
my $c = Affix::Build->new();
$c->add( \<<~'', lang => 'c' );
#include <stdio.h>
void write_recipe(FILE* stream, const char* title) {
if (stream) {
fprintf(stream, "RECIPE: %s\n", title);
fprintf(stream, "1. Preheat oven.\n");
fprintf(stream, "2. Mix ingredients.\n");
fflush(stream); // Important: C buffers IO. Flush if mixing with Perl IO.
}
}
my $lib = $c->link;
# 2. Bind the function
# CRITICAL STEP: Check your signature!
# Wrong: [ File, String ] <- Passes the whole skillet (Struct Copy)
# Right: [ Pointer[File], String ] <- Passes the handle (Pointer)
affix $lib, 'write_recipe', [ Pointer [File], String ] => Void;
# 3. Start Cooking
open my $fh, '>', 'dinner_plans.txt' or die "Oven broke: $!";
# Affix unwraps the Perl glob, extracts the FILE*, and hands it to C.
write_recipe( $fh, 'Spicy Meatballs' );
# You may use normal file I/O functions
close $fh;
# 4. Verify
print $^O eq 'MSWin32' ? `type dinner_plans.txt` : `cat dinner_plans.txt`;
Advanced: Multiplexing I/O (Perl & C Interleaved)
A common point of failure in FFI is buffering. Perl has its own IO buffers, and C stdio has its own. If you mix print (Perl) and fprintf (C) on the same handle, the output often appears out of order.
To handle this, we rely on flushing. In this example, we mix Perl's unbuffered syswrite with C's standard I/O.
use v5.40;
use Affix;
use Affix::Build;
# 1. Add a C function that writes a specific block
my $c = Affix::Build->new();
$c->add( \<<~'', lang => 'c' );
#include <stdio.h>
void c_append_middle(FILE* f) {
if (!f) return;
fprintf(f, "[C] ...Middle Content...\n");
fflush(f); // FLUSH is mandatory to sync with Perl's layer
}
my $lib = $c->link;
affix $lib, 'c_append_middle', [ Pointer [File] ] => Void;
open my $fh, '>', 'multiplex.log' or die $!;
# 1. Perl writes the Header
syswrite $fh, "[Perl] Header\n";
# 2. C writes the Body
# We pass the same handle. Affix extracts the FILE* from it.
c_append_middle($fh);
# 3. Perl writes the Footer
syswrite $fh, "[Perl] Footer\n";
close $fh;
# Verify order is preserved
say 'Multiplex Results:';
print do { local ( @ARGV, $/ ) = 'multiplex.log'; <> };
Advanced: Reading from C
You can also open a file in Perl (handling errors and paths in high-level code) and hand it off to C for parsing or reading.
use v5.40;
use Affix;
use Affix::Build;
my $c = Affix::Build->new();
$c->add( \<<~'', lang => 'c' );
#include <stdio.h>
// Reads a single char from the stream and returns it as an Int
int c_get_char(FILE* f) {
if (!f) return -1;
return fgetc(f);
}
my $lib = $c->link;
affix $lib, 'c_get_char', [ Pointer [File] ] => Int;
# Create a dummy file
open my $w, '>', 'input.txt';
print $w 'ABC';
close $w;
# Open for reading in Perl
open my $reader, '<', 'input.txt';
# Read byte 1 via C
say 'Read from C: ' . chr c_get_char($reader); # Output: A
# Read byte 2 via Perl
read $reader, my $buf, 1;
say 'Read from Perl: ' . $buf; # Output: B
# Read byte 3 via C
say 'Read from C: ' . chr c_get_char($reader); # Output: C
close $reader;
The PerlIO Interface
If you are interfacing with XS modules or Perl internals (someday...), the API might expect PerlIO* instead of standard C FILE*. Affix provides the Pointer[PerlIO] type for this.
While Pointer[PerlIO] behaves similarly to Pointer[File] in usage (it unwraps the handle), it creates a distinct type check in the signature to prevent mixing up abstract PerlIO streams with raw stdio streams.
use v5.40;
use Affix;
use Affix::Build;
use Fcntl qw[SEEK_CUR SEEK_SET];
my $c = Affix::Build->new();
$c->add( \<<~'', lang => 'c' );
// A function that theoretically accepts a PerlIO stream.
// Since we aren't linking libperl in this stub, we use void*
// to simulate an opaque handle, but in real XS, this would be PerlIO*.
void* identity(void* stream) {
return stream;
}
my $lib = $c->link;
# We define the signature using Pointer[PerlIO] to enforce intent
affix $lib, 'identity', [ Pointer [PerlIO] ] => Pointer [PerlIO];
open my $fh, '+<', 'input.txt';
syswrite $fh, "Test\n";
sysseek $fh, 0, SEEK_SET; # both handles will be at this position
# Round-trip the handle through C
my $returned_fh = identity($fh);
# Affix wraps the returned pointer in a new GLOB reference
say 'Original Handle: ' . $fh;
say 'Returned Handle: ' . $returned_fh;
say 'Original Handle Pos: ' . sysseek $fh, 0, SEEK_CUR;
say 'Returned Handle Pos: ' . sysseek $returned_fh, 0, SEEK_CUR;
say 'Reading returned handle: ' . <$returned_fh>; # Could just as easily read the original
say 'Original Handle Pos: ' . sysseek $fh, 0, SEEK_CUR;
say 'Returned Handle Pos: ' . sysseek $returned_fh, 0, SEEK_CUR;
Kitchen Reminders
-
Key takeaway: Multiplexing file IO will likely require you pay close attention to buffering. Use
fflush(f)or a similar function from C andsyswriteor 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 Filestruct FILEOpaque struct (Rarely used directly)