You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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 pathmy$lib_obj = Affix::load_library( $c->link );
packageNative::Widget {
use v5.40;
use Affix qw[:all];
# Closure over the library handle from the enclosing scopemy$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 Classsubnew($class) {
# Allocate the C++ objectmy$raw_ptr = widget_new();
# Tie the C++ destructor to the Perl object's lifecyclemy$managed_memory = main::wrap_owned( Affix::address($raw_ptr), Affix::address($dtor) );
# Store the managed memory inside our Perl objectreturnbless { _c_obj=>$managed_memory }, $class;
}
# 3. Dynamic Method Binding (Lazy VTable Lookup)subdo_work($self, $iterations) {
state $method;
# Extract the vtable function pointer on the first callif (!$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
The Facade
To a user of Native::Widget, there is no FFI. They simply call my $w = Native::Widget->new() and $w->do_work(5). The class is a plain blessed hash; all the vtable surgery happens inside the class, hidden from the consumer.
The Memory Anchor (wrap_owned) wrap_owned takes the raw integer address of the C++ object plus the raw integer address of a destructor shim and returns a blessed Affix::Memory object. When that object is garbage collected, its DESTROY calls the destructor shim (widget_delete), which in turn deletes the C++ object. By storing the Affix::Memory inside the $self hash, we anchor the C++ object's lifecycle to the Perl object: when $w falls out of scope, $self->{_c_obj} is destroyed and the C++ destructor fires automatically.
Dynamic Method Binding
The first do_work call reads the object's vtable. The first word of the object points to it and binds slot 2 (the virtual do_work) into a JIT trampoline via wrap. The signature leads with *void because the hidden this pointer arrives as the first argument.
Stateful Optimization
By using the state $method variable inside do_work, we only perform the heavy VTable lookup and JIT compilation on the first method call. Subsequent calls jump straight into the cached trampoline, providing immense performance.
Kitchen Reminders
wrap_owned lives in main:: wrap_owned (and alloc_owned) are installed into the main:: namespace by Affix's bootstrap, so inside your own package you must call them fully qualified: main::wrap_owned(...). This is the same helper the test suite uses to manage C++ destructor lifetimes.
address() unwraps pins to integers wrap_owned wants plain integer addresses. Affix::address($pin) returns the raw UV of a pinned pointer, and for a symbol pin from find_symbol it returns the symbol's value. Note that find_symbol requires an Affix::Lib object. Call Affix::load_library($path) first; a bare path string will not work.
VTable cast pattern
Use cast($obj, Pointer[Size_t]), dereference with $$ to get the vtable address, then cast($vtable_addr, Array[ Size_t, 3 ]) to index the slots. Do not cast to Pointer[Pointer[...]] or Pointer[Array[...]]; those return a scalar pin, and indexing it raises Not an ARRAY reference (see Chapter 57).
wrap and the signature string wrap's prototype ($$$;$) blocks the two-argument form, so always pass three arguments: wrap( undef, $func_addr, '(*void,int)->void' ). The signature string uses C syntax (*void, int). The String type object is only valid in [args] => ret pairs.
Passing Affix::Memory as an argument
The marshaller recognizes an Affix::Memory object as a pointer handle, so you can hand $self->{_c_obj} straight to the JIT trampoline as the this pointer.
ABI Fragility
As I mentioned in previous chapters, vtable layouts are ABI-dependent. The recipe assumes the Itanium ABI (GCC/Clang/MinGW) where slot 2 is the first user-defined virtual method. MSVC on Windows may place it elsewhere. Always double-check your target platform's layout.
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
In previous chapters, we hacked a C++ vtable and learned to use
ThisCallto pass the object context smoothly. Now, we're going to combine those techniques withwrap_ownedto 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++
Widgetclass. We will wrap it in a pure Perl class.Which prints:
How It Works
To a user of
Native::Widget, there is no FFI. They simply callmy $w = Native::Widget->new()and$w->do_work(5). The class is a plain blessed hash; all the vtable surgery happens inside the class, hidden from the consumer.wrap_owned)wrap_ownedtakes the raw integer address of the C++ object plus the raw integer address of a destructor shim and returns a blessedAffix::Memoryobject. When that object is garbage collected, itsDESTROYcalls the destructor shim (widget_delete), which in turndeletes the C++ object. By storing theAffix::Memoryinside the$selfhash, we anchor the C++ object's lifecycle to the Perl object: when$wfalls out of scope,$self->{_c_obj}is destroyed and the C++ destructor fires automatically.The first
do_workcall reads the object's vtable. The first word of the object points to it and binds slot 2 (the virtualdo_work) into a JIT trampoline viawrap. The signature leads with*voidbecause the hiddenthispointer arrives as the first argument.By using the
state $methodvariable insidedo_work, we only perform the heavy VTable lookup and JIT compilation on the first method call. Subsequent calls jump straight into the cached trampoline, providing immense performance.Kitchen Reminders
wrap_ownedlives inmain::wrap_owned(andalloc_owned) are installed into themain::namespace by Affix's bootstrap, so inside your ownpackageyou must call them fully qualified:main::wrap_owned(...). This is the same helper the test suite uses to manage C++ destructor lifetimes.address()unwraps pins to integerswrap_ownedwants plain integer addresses.Affix::address($pin)returns the rawUVof a pinned pointer, and for a symbol pin fromfind_symbolit returns the symbol's value. Note thatfind_symbolrequires anAffix::Libobject. CallAffix::load_library($path)first; a bare path string will not work.Use
cast($obj, Pointer[Size_t]), dereference with$$to get the vtable address, thencast($vtable_addr, Array[ Size_t, 3 ])to index the slots. Do not cast toPointer[Pointer[...]]orPointer[Array[...]]; those return a scalar pin, and indexing it raisesNot an ARRAY reference(see Chapter 57).wrapand the signature stringwrap's prototype ($$$;$) blocks the two-argument form, so always pass three arguments:wrap( undef, $func_addr, '(*void,int)->void' ). The signature string uses C syntax (*void,int). TheStringtype object is only valid in[args] => retpairs.Affix::Memoryas an argumentThe marshaller recognizes an
Affix::Memoryobject as a pointer handle, so you can hand$self->{_c_obj}straight to the JIT trampoline as thethispointer.As I mentioned in previous chapters, vtable layouts are ABI-dependent. The recipe assumes the Itanium ABI (GCC/Clang/MinGW) where slot 2 is the first user-defined virtual method. MSVC on Windows may place it elsewhere. Always double-check your target platform's layout.
All reactions