A native Cairo binding, shipped#

The vector chapter drew with Cairo as an ordinary CPAN module. This chapter shows the other side of that same code: pperl ships a native reimplementation of Cairo, so use Cairo works on pperl with no CPAN Cairo distribution installed and no C compiler involved. The Perl you write is identical; what backs it is pperl’s own FFI reaching libcairo directly. Cairo makes the ideal worked example because its C API is almost entirely scalars and one opaque handle, which is precisely the shape that reaches cleanly today.

This is a real, shipped binding, not a sketch. Reading it tells you what a curated native graphics binding is, how the pieces fit, and how to recognize (or ask for) the same treatment for another library. Building such a binding is a runtime task; what you write as a user is the Perl on the far side of it, and that Perl is just the Cairo API you already saw.

The Perl API: exactly the CPAN one#

The native binding exposes the real Cairo package names and methods: Cairo::ImageSurface, Cairo::Context, Cairo::PdfSurface, and the rest, with the upstream method signatures. Code written for CPAN Cairo runs unchanged; ported Cairo test suites pass against it. The program from the vector chapter is the program here:

use Cairo;

my $surface = Cairo::ImageSurface->create('argb32', 200, 200);
my $cr      = Cairo::Context->create($surface);

$cr->set_source_rgb(1, 1, 1);
$cr->paint;                                  # white background
$cr->set_source_rgb(0, 0, 0);
$cr->set_line_width(3);
$cr->move_to(20, 20);
$cr->line_to(180, 20);
$cr->stroke;
$cr->arc(100, 100, 40, 0, 6.28318);
$cr->set_source_rgba(1, 0, 0, 0.5);
$cr->fill;

$surface->write_to_png('out.png');
# the surface and context free their C objects when they go out of scope

Nothing in that program hints at how it is implemented. There is no dlopen, no signature string, no binding object: just Cairo. The rest of this chapter is what makes use Cairo do that on pperl, and the pattern is the same one behind Math::GMP.

Step 1: open the library once, resolve the symbols#

The binding opens libcairo.so.2 a single time, when the module loads, and resolves each C function it needs into a table of function pointers held for the life of the process. This is the same shape as the Math::GMP binding, which opens libgmp once and resolves its mpz_* symbols. For Cairo the resolved symbols are the ones behind the methods above:

open libcairo.so.2 once when the module loads
resolve and store, as function pointers:
    cairo_image_surface_create(format, width, height) -> surface*
    cairo_create(surface*)                             -> context*
    cairo_move_to(context*, x, y)
    cairo_line_to(context*, x, y)
    cairo_arc(context*, xc, yc, radius, a1, a2)
    cairo_set_source_rgb(context*, r, g, b)
    cairo_set_source_rgba(context*, r, g, b, a)
    cairo_set_line_width(context*, w)
    cairo_stroke(context*)  cairo_fill(context*)  cairo_paint(context*)
    cairo_surface_write_to_png(surface*, path) -> status
    cairo_destroy(context*)  cairo_surface_destroy(surface*)

If libcairo is not installed on the system, the binding reports it once with a helpful message rather than crashing. Every one of those C functions has a signature the FFI’s type codes already express: the arguments are the opaque handle (an address, so an integer), doubles, and a string path. There is no by-value struct in the core drawing API, which is why Cairo is the clean example.

Cairo is a large library, so the real binding is organized by package: Cairo::Surface, Cairo::Context, Cairo::Path, Cairo::Pattern, Cairo::Matrix, Cairo::Font, and Cairo::Region each resolve the symbols they need. That is an implementation convenience; the user sees one Cairo.

Step 2: the opaque handle as a blessed integer#

Cairo hands back a cairo_surface_t* and a cairo_t*: pointers to C structs whose insides you never touch. The binding does not need to understand their layout, only to hold the pointer and pass it back on every call. pperl represents such a handle the same way Math::GMP holds its mpz_t pointer: as an integer inside a blessed reference.

Cairo::ImageSurface object  =  bless(\ (surface_ptr as integer), 'Cairo::ImageSurface')
Cairo::Context object       =  bless(\ (context_ptr as integer), 'Cairo::Context')

When a method is called, the binding reads the integer back out of the blessed reference, casts it to the C pointer, and calls the function. The blessing gives you method dispatch ($cr->stroke), inheritance (every concrete surface class is a Cairo::Surface subclass), and, crucially, a DESTROY hook. The only difference from Math::GMP is that the opaque thing is a drawing surface instead of a big integer.

Step 3: lifetime tied to DESTROY#

A C graphics object must be freed exactly once. Leak it and you burn memory every drawing; free it twice and you corrupt the heap. The binding ties the C object’s life to the Perl object’s: when the blessed reference goes out of scope, Perl calls DESTROY, and the binding calls the matching C destructor, respecting cairo’s reference counting.

Cairo::Context DESTROY  ->  cairo_destroy(context_ptr)
Cairo::Surface DESTROY  ->  cairo_surface_destroy(surface_ptr)

Math::GMP does precisely this: its destructor calls mpz_clear and frees the mpz_t. You, the user, never call a free function; the drawing objects clean up when the Perl variables holding them go away. The binding enforces the ordering cairo requires, releasing a context before the surface it draws to.

Step 4: the methods#

Each Perl method is a small native function that reads the handle, reads the scalar arguments, and makes one C call. Cairo::Context::move_to reads the context pointer and two doubles and calls cairo_move_to; Cairo::Context::set_source_rgba reads the pointer and four doubles and calls cairo_set_source_rgba; Cairo::Surface::write_to_png reads the pointer and a path string. The mapping is one method to one C call:

Cairo::Context::move_to($cr, $x, $y)
    -> read context ptr from $cr; call cairo_move_to(ptr, x, y)

Cairo::Context::set_source_rgba($cr, $r, $g, $b, $a)
    -> call cairo_set_source_rgba(ptr, r, g, b, a)

Cairo::Surface::write_to_png($surface, $path)
    -> call cairo_surface_write_to_png(ptr, path)

The C-level signatures live in the binding and are settled there once. A user calling $cr->set_source_rgba(1, 0, 0, 0.5) never writes a signature and cannot get one wrong. That single point of truth for the C calls is the third of the three reasons a curated binding beats scattered raw calls: object lifetime, ergonomics, and the signature written down once.

What this reaches, and what it does not#

With those four pieces (open once, handle as a blessed integer, DESTROY frees, methods over the resolved function table) pperl delivers a real, memory-safe Cairo with no CPAN distribution installed, no C compiler, and no build step. The full vector chapter drawing model is available, and the objects manage themselves.

What the pattern does not, by itself, reach is the same frontier the primer named. Cairo avoids all of it in its core drawing API, which is why it is the first graphics library brought up this way. A library that passes colors or points by value as structs, that calls back into your code, or that hands you union-typed events needs more than this pattern has: the by-value and union cases wait on the data-description layer, and callbacks need function-pointer support that re-enters Perl. The 2D game capstone hits exactly that wall with windowing and event libraries, and says so at the point it does.

The takeaway#

A graphics library whose C API is scalars plus opaque handles can be brought up on pperl as a curated native binding, and Cairo is the first one shipped: open the library once, hold each C object as a blessed integer, free it in DESTROY, and back each method with its C call settled in one place. The result is the upstream Cairo API, running natively. Other libraries of the same shape are on the way. The next chapter stops describing and starts building: a drawing application on the maintained toolkit stack, assembling the whole guide into something that runs.