In previous chapters, we hacked a C++ vtable and learned to use ThisCall to pass the object context smoothly. Now, we're going to combine those techniques with wrap_owned to create the Universal Object Pattern.

This pattern completely hides the C++ FFI boundary from your end-users. They interact with a standard Perl object, and behind the scenes, Affix manages the memory lifecycle and translates the method calls.

The Recipe

Let's assume we have an opaque C++ Widget class. We will wrap it in a pure Perl class.

use v5.40;
use Affix qw[:all];
use Affix::Build;

# Compile a tiny C++ library with a Widget class (C shims only)
my $c = Affix::Build->new();
$c->add( \<<~'CPP', lang => 'cpp' );
    #include <iostream>

    class Widget {
    public:
        virtual ~Widget() {
            std::cout << "Widget destroyed" << std::endl;
        }
        virtual void do_work(int iterations) {
            for (int i = 0; i < iterations; ++i)
                std::cout << "Widget::do_work iteration " << i << std::endl;
        }
    };

    extern "C" {
        Widget* widget_new()    { return new Widget(); }
        void    widget_delete(Widget* w) { delete w; }
    }
CPP

# find_symbol() needs an Affix::Lib object, not a bare path
my $lib_obj = Affix::load_library( $c->link );

package Native::Widget {
    use v5.40;
    use Affix qw[:all];

    # Closure over the library handle from the enclosing scope
    my $LIB = $lib_obj;

    # 1. Bind the C shims that spawn the object
    affix $LIB, 'widget_new', [] => Pointer[Void];
    my $dtor = Affix::find_symbol( $LIB, 'widget_delete' );

    # 2. Construct the Class
    sub new($class) {
        # Allocate the C++ object
        my $raw_ptr = widget_new();

        # Tie the C++ destructor to the Perl object's lifecycle
        my $managed_memory = main::wrap_owned( Affix::address($raw_ptr), Affix::address($dtor) );

        # Store the managed memory inside our Perl object
        return bless { _c_obj => $managed_memory }, $class;
    }

    # 3. Dynamic Method Binding (Lazy VTable Lookup)
    sub do_work($self, $iterations) {
        state $method;

        # Extract the vtable function pointer on the first call
        if (!$method) {
            my $vptr_ref    = cast( $self->{_c_obj}, Pointer[Size_t] );
            my $vtable_addr = $$vptr_ref;
            my $vtable      = cast( $vtable_addr, Array[ Size_t, 3 ] );

            # Let's assume do_work is virtual method #2 (Itanium ABI)
            my $func_addr = $vtable->[2];

            $method = wrap( undef, $func_addr, '(*void,int)->void' );
        }

        # Execute the C++ method, passing the native object pointer first
        $method->( $self->{_c_obj}, $iterations );
    }
}

# 4. Use it!
my $w = Native::Widget->new();
$w->do_work(3);
undef $w;    # Perl object destroyed -> C++ destructor fires

Which prints:

Widget::do_work iteration 0
Widget::do_work iteration 1
Widget::do_work iteration 2
Widget destroyed

How It Works

Kitchen Reminders