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.

This is not FFI::Platypus. Peta::FFI is built into the runtime, and its curated bindings are native Rust, not Perl-side declarations. The distinction 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 three levels#

Peta::FFI is best understood as three levels, from raw to curated.

Level 1, 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, and P a mutable output buffer. That set covers a surprising amount of a C API: anything whose arguments and return are scalars, strings, or a byte buffer.

Level 2, a data-description layer. Many C functions take or return structs, not scalars, and the raw type codes cannot describe a struct’s layout. The planned answer is a declarative layer where you describe a record’s fields once and let the FFI marshal it. This is the emerging middle path; it is not a shipping module yet, and this guide is explicit at each point about where a binding would need it.

Level 3, curated bindings. The top level is a hand-written native binding: a Rust 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. 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#

The dividing line for graphics is the struct. A great deal of a C graphics API is reachable at Level 1 right now, because it is scalars and opaque handles:

  • Functions that take and return numbers (set a line width, a radius, a color 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.

  • An opaque handle returned by the library (a surface pointer, a context pointer) is just an address. pperl already represents a library handle as an integer, and the curated pattern in the next chapter stores these object handles the same way: as an integer inside a blessed Perl reference.

Where raw calls stop:

  • Structs passed or returned by value. A function taking a Color { r, g, b, a } by value, or returning a Rectangle, cannot be described by the scalar type codes. This is the Level 2 gap.

  • Callbacks. A library that calls back into your code (an event loop that invokes a handler, a draw signal) needs a C function pointer that re-enters Perl. That is beyond the raw layer.

  • Unions. An event type that is a union of many struct shapes (the classic windowing event record) combines both problems above.

These are not permanent walls; they are the frontier the FFI layers are growing toward. This guide’s value is telling you exactly which side of the line a given task falls on, so you neither avoid what works nor promise what does not.

Why a curated binding, not raw calls, for real work#

You could drive a graphics library entirely through Level 1 calls, and for a quick experiment that is fine. For anything real, a Level 3 curated 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 curated 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. Raw calls leave that entirely to you.

First-class Perl ergonomics is the second reason: $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 curated binding the "(ppd)i" strings live in one Rust module, checked once; 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 builds a curated Cairo binding the way Math::GMP is built, so you can see a graphics library brought up on pperl end to end. Then the two capstones use the maintained toolkit stack from Arc B, anchored on Prima, to build a drawing application and a 2D game you can run, with the FFI thread woven in where it earns its place and honestly flagged where the frontier begins. The final chapter draws the map of what is left.