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
-
1. The Memory Layout
$this (0x1000) .-----------. | vptr | ------> vtable (0x5000) +-----------+ +--------------------------+ | member... | | [0] Destructor | `---------' | [1] Destructor (Deleting)| | [2] say_hello() <-- TARGET | [3] add() <-- TARGET +--------------------------+ -
2. Double Indirection
To get the function address, we must dereference twice:
- Read memory at
$thisto get$vtable_addr. - Read memory at
$vtable_addr + offsetto get the function address.
We achieve this in Affix by casting
$thistoPointer[Size_t]to read the pointer-sized integer at that location. - Read memory at
-
3. Wrapping Raw Pointers
my $sub = wrap( $address, [Args] => Ret );The
wrapfunction usually takes a library handle. However, if you pass a raw integer address (or a Pin) as the first argument, Affix treats it as the function address itself. This converts the raw vtable entry into a callable Perl subroutine.
Kitchen Reminders
-
Fragility
This relies on the Itanium C++ ABI (Linux/macOS/MinGW) or the MSVC ABI (Visual Studio). The layout changes if you use Multiple Inheritance or Virtual Inheritance. This recipe works best for simple classes or COM-style interfaces (which use pure virtual classes).
-
The 'this' Argument
C++ methods are just C functions with a hidden first argument (
this). When binding manually, you must include this argument in the signature (Pointer[Void]) and pass the object instance every time you call the method.