Reaching further: Peta::FFI for graphics#
The toolkit chapters covered what CPAN already wraps. This chapter, and the rest of the guide, goes further: reaching a C graphics library that has no CPAN binding at all, using pperl’s native FFI. This is a pperl-exclusive capability. Binding a C library the traditional way means writing a C shim, compiling it, and shipping a build toolchain with your code; pperl calls the C library directly, the same mechanism that already gives you Math::GMP with no compiled binding to build at all.
Peta::FFI is built into the runtime, and so are the native bindings built on its binding layer: they are Rust modules, not Perl-side declarations. (pperl’s built-in FFI::Platypus is a layer over Peta::FFI’s engine, for code written for CPAN’s FFI::Platypus.) This matters for graphics because a real graphics binding needs to manage C-allocated objects (a surface, a context) across many calls, and pperl’s model handles that the way the C library itself does.
The authoritative reference for the FFI layer is the Auto-FFI page; this chapter applies it to graphics specifically.
The two levels#
Peta::FFI is best understood as two levels: raw calls you write yourself, and native bindings someone has written for you.
Raw calls. You name a library, a function, and a type-signature string, and pperl dispatches the call through libffi. Nothing is generated ahead of time; any signature you can spell works.
use Peta::FFI qw(dlopen call dlclose);
my $lib = dlopen("libm.so.6");
my $r = call($lib, "sqrt", "(d)d", 2.0); # sqrt(2) = 1.4142...
dlclose($lib);
The signature "(args)ret" uses one letter per type: v void, i int, l long, L unsigned long or size_t, d double, f float, p a C string in, P a mutable output buffer, and o an opaque pointer carried as a plain integer. Around those codes sit two more sets of primitives, and they are what make the rest of a graphics API reachable, beyond the functions that take plain scalars: alloc, free, peek, peek_cstr, poke, pack_ptr and unpack_ptr for memory you own, and closure and closure_free for handing a Perl coderef to C as a function pointer.
Native bindings. The other level is a hand-written native binding, built on Peta::FFI’s binding layer: a module that opens the library once, resolves every symbol it needs, and presents a clean Perl API, managing the C-allocated objects behind Perl values. It is the faster and more comfortable form of the same reach. Math::GMP is exactly this: use Math::GMP and you get arbitrary-precision integers with operator overloading, and nowhere did you touch dlopen or a signature string. The next chapter takes one of these that pperl already ships for a graphics library, Cairo, and shows how it works.
What raw calls can and cannot reach#
Most of a C graphics API is reachable right now, because most of it is scalars, handles, and memory the caller owns:
Functions that take and return numbers (set a line width, a radius, a colour component) map straight onto
i,d,f, and friends.Functions that take a C string (load a font by name, open a file) use
p.Functions that fill a caller-provided buffer (read pixels into memory you own) use
P, orallocplusowhen you want to keep the buffer across several calls.An opaque handle returned by the library (a surface pointer, a context pointer) is an address, and
ois its type code. Declared withhandle_type, it becomes an object of a class of your own that frees itself, as a<Class>type in the signature. The native pattern in the next chapter stores these object handles the same way: as an integer inside a blessed Perl reference.A struct, passed or returned by value (a
Color { r, g, b, a }, aRectangle) or through a pointer (a rectangle out-parameter), is reachable.recorddeclares it member by member, Peta::FFI lays it out as the C compiler does, and the signature names it as{Color}by value or*{Color}by pointer.A union (an event record) is reachable too, by pointer:
uniondeclares it asrecorddeclares a struct, every member at offset 0, and the signature passes it as*{Event}for the library to fill in. A union cannot be passed by value.Arrays and out-parameters take a Perl reference:
\@pointsfor aT[],\my $countfor a*T,\$buffor aPbuffer C fills.A callback is reachable.
closurewraps a coderef as a genuine C function pointer, so a library that calls into your code (a comparator, a progress handler, an event or draw callback) gets one.
Where raw calls stop: anything derived from a header. Nothing reads the library’s .h files for you. You write the signature from the C prototype yourself and declare each record’s and union’s members yourself. Getting those right is your responsibility. A signature that disagrees with the real prototype is undefined behaviour rather than an error you get told about.
The practical shape of that: the mechanism for a graphics library is in place, and what a hand-written binding costs you now is the careful work of transcribing prototypes and declarations correctly. This guide’s value is telling you which side of the line a given task falls on, so you neither avoid what works nor promise what does not.
Why a native binding, not raw calls, for real work#
You could drive a graphics library entirely through raw calls, and for a quick experiment that is fine. For anything real, a native binding is the right unit, for three reasons.
First, object lifetime. A graphics context is allocated by the library and must be freed exactly once. A native binding ties that lifetime to a Perl value’s DESTROY, so the C object is released when the Perl object goes away, with no manual bookkeeping. With raw calls you declare that yourself, with handle_type for each C object type, or keep track of every o pointer by hand.
Second, ergonomics. $surface->fill reads better and is harder to get wrong than a call with a hand-written signature at every use site.
Third, the signature is written once. In a native binding each C function’s exact type is declared once, in one Rust module, and the compiler checks every argument against it; every caller just uses the method. Scattering raw signatures through application code is how you get a mismatch that corrupts the stack.
The Cairo showcase makes all three concrete with the Cairo binding pperl ships, a real vector library whose API is almost entirely scalars and one opaque handle, which is precisely the shape that reaches cleanly today.
The shape of what follows#
The next chapter opens up the native Cairo binding pperl ships, built the way Math::GMP is, so you can see a graphics library brought up on pperl end to end. Then the two capstones build a drawing application and a 2D game you can run, both on the native SDL2 binding, with the raw layer shown where it clarifies what the binding removes. The final chapter draws the map of what is left.