Affix cannot directly instantiate a C++ class or read a C struct unless we define every single field with the correct type and in the proper order. Often, we don't care about the fields, we just want to hold onto the object and pass it back to the library later. In this case, we can treat the object as a black box, an opaque pointer (Pointer[Void]) and use helper functions to interact with it.
The Recipe
We will create a simple C++ Person struct, wrap it with C-compatible helpers, and bind it to Perl.
use v5.40;
use Affix qw[:all];
use Affix::Build;
# 1. Compile the C++ Library
my $c = Affix::Build->new();
$c->add( \<<~'END', lang => 'cpp' );
#include <stdlib.h>
#include <string.h>
typedef struct {
char * name;
unsigned int age;
} Person;
// extern "C" prevents C++ name mangling. This ensures the symbols in the
// DLL are just "person_new", not "_Z10person_newPKcj".
extern "C" {
Person * person_new(const char * name, unsigned int age) {
Person * self = (Person *) malloc(sizeof(Person));
self->name = strdup(name);
self->age = age;
return self;
}
const char * person_name(Person * self) {
return self->name;
}
unsigned int person_age(Person * self) {
return self->age;
}
void person_free(Person * self) {
if (self) {
free(self->name);
free(self);
}
}
}
END
my $lib = $c->link;
# 2. Define the Opaque Type
# We tell Affix: "Whenever you see 'Person', treat it as a void pointer."
typedef Person => Pointer [Void];
# 3. Bind
# Notice we use Person() as the type.
affix $lib, 'person_new', [ String, UInt ] => Person();
affix $lib, 'person_name', [ Person() ] => String;
affix $lib, 'person_age', [ Person() ] => UInt;
affix $lib, 'person_free', [ Person() ] => Void;
# 4. Use
my $roger = person_new( 'Roger Frooble Bits', 35 );
# $roger is just a blessed scalar holding a memory address.
# We pass it back to C to get data.
say 'Name: ' . person_name($roger);
say 'Age: ' . person_age($roger);
# Clean up
person_free($roger);
How It Works
-
1.
extern "C"C++ compilers "mangle" function names (e.g.,
person_newmight become_Z10person_newPKcj) to support function overloading. This is a nightmare to bind and depends on the ABI of the compiler. Wrapping your helper functions inextern "C"forces compilers to use standard C names. -
2. The shim pattern
We didn't bind the C++ struct fields directly. Instead, we wrote small C helper functions (shims) to access the data:
const char * person_name(Person * self) { return self->name; }This insulates you from changes in the C++ class layout.
-
3. Typedef aliases
typedef Person => Pointer[Void];This creates a named alias. While
Pointer[Void]works, usingPerson()in your signatures makes the code self-documenting. If you later defineCar => Pointer[Void], Affix won't stop you from passing aCarto aPersonfunction (since they are both just pointers), but your code will be much easier to read and debug.
Kitchen Reminders
-
Constructors must return
It seems obvious but, in C++, forgetting to
return self;in a constructor function is easy to do and leads to immediate segfaults or illegal instruction errors. -
Memory ownership
Since
person_newusedmalloc, the memory belongs to the C heap. You must explicitly callperson_freeto release it. Affix cannot garbage collect these opaque pointers automatically unless you attach a destructor using Perl'sDESTROY(which is an advanced topic I might cover in another chapter).