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

Kitchen Reminders