C++ libraries are notoriously difficult for FFI systems to bind. Unlike C, which uses standard symbol names, C++ "mangles" function names (for example, turning Warrior::attack() into _ZN7Warrior6attackEv) to support features like overloading. Furthermore, C++ methods require a hidden this pointer to know which object instance they are acting upon.
To capture a C++ class, we must bridge the gap by creating a C shim. This is a thin layer of extern "C" functions that expose the C++ class creation, destruction, and methods as standard C functions. This is the cheap and easy route.
The Recipe
This time, we're creating a simple C++ Droid class, wrap it in a C-compatible API, and then build a Perl class to manage the object's lifecycle automatically.
use v5.40;
use Affix qw[:all];
use Affix::Build;
$|++;
# 1. The C++ Class and C Shim
# We include both the class definition and the extern "C" wrappers.
my $src = <<~'END_CPP';
#include <iostream>
/* Pure C++ Class */
class Droid {
int serial;
public:
Droid(int s) : serial(s) {}
~Droid() {
std::cout << "Droid " << serial << " is being scrapped." << std::endl;
}
void speak() {
std::cout << "Droid " << serial << " says: Roger roger." << std::endl;
}
};
/* The C ABI Shim */
extern "C" {
// Constructor Wrapper
void* Droid_new(int serial) {
return new Droid(serial);
}
// Method Wrapper (Pass object pointer explicitly)
void Droid_speak(void* d) {
static_cast<Droid*>(d)->speak();
}
// Destructor Wrapper
void Droid_delete(void* d) {
delete static_cast<Droid*>(d);
}
}
END_CPP
# 2. Compile
# Affix::Build detects C++ by file extension or flags.
# We'll treat this string as .cpp content.
my $c = Affix::Build->new;
$c->add( \$src, lang => 'cpp' );
my $lib = $c->link;
# 3. Define the Perl Class
package Droid {
use Affix qw[:all];
# Define the pointer type for clarity
# We treat the C++ object as an opaque Void Pointer in Perl
typedef DroidPtr => Pointer [Void];
# Bind the Shim functions
affix $lib, 'Droid_new', [Int] => DroidPtr();
affix $lib, 'Droid_speak', [ DroidPtr() ] => Void;
affix $lib, 'Droid_delete', [ DroidPtr() ] => Void;
sub new( $class, $serial ) {
# Call C++ new, get the pointer
my $ptr = Droid_new($serial);
# Bless the pointer into a reference to make it a Perl object
return bless \$ptr, $class;
}
sub speak($self) {
# Dereference to get the raw pointer and pass to shim
Droid_speak($$self);
}
sub DESTROY($self) {
# Automatically clean up C++ memory when Perl object dies
Droid_delete($$self);
}
}
# 4. Run it
{
say 'Fabricating unit...';
my $r2 = Droid->new(101);
say 'Commanding unit...';
$r2->speak();
say 'Unit going out of scope...';
}
say 'Done.';
Output:
Fabricating unit...
Commanding unit...
Droid 101 says: Roger roger.
Unit going out of scope...
Droid 101 is being scrapped.
Done.
How It Works
-
1. The
extern "C"BlockThis tells the C++ compiler: "Do not mangle these names." This ensures
Droid_newappears in the library exactly as written, making it findable by Affix. -
2. The Opaque Pointer (
void*)Perl does not need to know the layout of the
class Droid. To Perl, the object is just a memory address. In the C shim, we acceptvoid*andstatic_castit back toDroid*to call methods. -
3. Lifecycle Management
- Construction:
Droid_newallocates memory on the C++ heap (new Droid). - Destruction: We bind
Droid_deleteto Perl'sDESTROYmethod. When the Perl variable$r2goes out of scope, Perl callsDESTROY, which callsdeletein C++, preventing memory leaks.
- Construction:
Kitchen Reminders
-
Catching Exceptions
You must catch all C++ exceptions within your C shim. If a C++ exception throws across the FFI boundary into Perl, your program will crash instantly. Use
try { ... } catch (...) { ... }blocks inside your shim functions to handle errors gracefully (perhaps returning an error code). -
Virtual Methods
If you need to extend a C++ class in Perl (i.e., override a virtual method in Perl), the C shim must be more complex. You would do something like create a C++ child class that holds a Perl callback, forwarding the virtual call back to Perl.