In Chapter 30, we wrapped C++ classes using extern "C" helper functions in a shim. That is the 'Right Way.' But what if you are stuck with a pre-compiled C++ library and can't recompile it? What if you're just super bored? Let's say you have an object pointer, but no C functions to pass it to.

To call a method, we must act like a C++ compiler: manually look up the Virtual Method Table (vtable) and call the function pointer directly.

The Recipe

You know the drill. Let's create a C++ class, then write a Perl script that "scans" the object to find and call its methods.

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

# Compile C++ Library
# We define a class with virtual methods.
my $c = Affix::Build->new();
$c->add( \<<~'END', lang => 'cpp' );
    #include <stdio.h>

    class Calculator {
    public:
        // Explicit virtual destructor
        // (Often occupies vtable slots 0 & 1 on GCC/Clang)
        virtual ~Calculator() {}

        // Target Methods
        virtual void say_hello(const char* name) {
            printf("Hello, %s!\n", name);
        }

        virtual int add(int a, int b) {
            return a + b;
        }
    };

    extern "C" Calculator* new_calc() {
        return new Calculator();
    }
END
my $lib = $c->link;

# 2. Get the Object
affix $lib, 'new_calc', [] => Pointer [Void];
my $this = new_calc();

# Find the VTable
# The first 8 bytes (on 64-bit) of a C++ object are usually the vptr.
# Structure: $this -> [ vptr ] -> [ func0, func1, func2 ... ]
# Cast $this to a Pointer to a Size_t (Pointer-sized integer).
# This allows us to read the address stored in the first 8 bytes.
my $vptr_ref = cast( $this, Pointer [Size_t] );

# Dereference to get the address of the vtable array.
my $vtable_addr = $$vptr_ref;
say sprintf 'Object: 0x%X', address($this) ;
say sprintf 'VTable: 0x%X', $vtable_addr ;

# Scan the VTable
# We interpret the vtable as an array of 5 pointers (Size_t).
my $slots_pin = cast( $vtable_addr, Array [ Size_t, 5 ] );
my $addrs     = $$slots_pin;

# Dump the slots to find our functions.
# Code pointers will look like valid memory addresses.
# Slots 0/1 are often destructors or RTTI info.
for my $i ( 0 .. 4 ) {
    say sprintf( "Slot [%d]: 0x%X", $i, $addrs->[$i] );
}

# Heuristic:
# GCC/Clang (Itanium ABI): Slots 0,1 are dtors. Methods start at 2.
# MSVC (Windows ABI): Methods usually start at 0 or 1.
my ( $idx_hello, $idx_add ) = ( 2, 3 );

# Simple detection for MSVC
if ( $^O eq 'MSWin32' && $Config{cc} =~ /cl/i ) {
    ( $idx_hello, $idx_add ) = ( 0, 1 );
}
my $addr_hello = $addrs->[$idx_hello];
my $addr_add   = $addrs->[$idx_add];

# Bind and Call
# wrap() can take a raw integer address instead of a library handle.
#
# CRITICAL: C++ methods pass 'this' as the hidden first argument!
# We must include Pointer[Void] at the start of the signature.
if ( $addr_hello && $addr_add ) {
    my $say_hello = wrap( $addr_hello, [ Pointer [Void], String ] => Void );
    my $add       = wrap( $addr_add,   [ Pointer [Void], Int, Int ] => Int );

    # Execute
    $say_hello->( $this, 'Perl Hacker' );
    say '10 + 20 = ' . $add->( $this, 10, 20 );
}
else {
    say 'Could not resolve function addresses.';
}

The output looks like this:

Object: 0x11ECCA01160
VTable: 0x7FFBF85E4360
Slot [0]: 0x7FFBF85E2E40
Slot [1]: 0x7FFBF85E2E10
Slot [2]: 0x7FFBF85E2DC0
Slot [3]: 0x7FFBF85E2DA0
Slot [4]: 0x694D28203A434347
10 + 20 = 30
Hello, Perl Hacker!

How It Works

Kitchen Reminders