XS::Framework::Manual::recipe01 - XS::Framework basics
Let's assume that there is an external C++ class, which we'd like to adapt to Perl. Let it be the simple date structure, which encapsulates Unix epoch:
struct DateRecipe01a { DateRecipe01a() { update(false) ; } ~DateRecipe01a() { std::cerr << "~DateRecipe01a()\n"; } void update(bool trace = true) { if (trace) std::cerr << "DateRecipe01a::update()\n"; epoch = std::time(nullptr); } int get_epoch() const { return epoch; } private: std::time_t epoch; };
First of all there should be a typemap for DateRecipe01a. It is recommended to have an external .h file, however for the bravety it is embedded into .xsi file:
DateRecipe01a
.h
.xsi
#include <xs.h> // (1) #include <my-library.h> // (2) namespace xs { // (3) template <> // (4) struct Typemap<DateRecipe01a*> : TypemapObject<DateRecipe01a*, DateRecipe01a*, ObjectTypePtr, ObjectStorageIV, StaticCast> { // (5) (6) (7) (8) (9) (10) (11) (12) static std::string package () { return "MyTest::Cookbook::DateRecipe01a"; } // (13) (14) }; }
The XS::Framework C++ headers should be included as shown in (1). Then the target library headers should be included (2).
Then the typemap (aka Perl-C++ mapping rules for particular class) should be defined: it is done as Typemap class (5) full specialization (4) in the xs namespace (3). This is similar how hash-function is for std::unordererd_hash is specialized in namespace std for user supplied classes.
typemap
Typemap
xs
std::unordererd_hash
std
We decided to use pointer-semantics (6), i.e. create in C++ code the DateRecipe01a objects on heap and return them to Perl. As specializing Typemap from scratch is non-trivial job, it is recommended to use the supplied helper TypemapObject (7).
TypemapObject
There is no class inheritance in the current recipe, so basic class DateRecipe01a* (8) matches final class DateRecipe01a* (9).
DateRecipe01a*
We decided to follow commonly used pattern in CPAN-modules, i.e. LifetimePolicy = ObjectTypePtr (10), which transfers object ownernership to Perl via pointer and invoke delete DateRecipe01a* when perl wrapper goes out of life; and StoragePolicy = ObjectStorageIV (11) to use integer field of perl SV* to store pointer to DateRecipe01a*. As the C++ class <DateRecipe01a*> does not participates in virtual inheritance in accordance with C++ rules it is possible to use static_cast(12) policy for pointer; it is the fastest approach as it has zero runtime costs.
LifetimePolicy
ObjectTypePtr
delete DateRecipe01a*
StoragePolicy
ObjectStorageIV
static_cast
The package method (13) defines the blessed package name (14) via which the DateRecipe01a is accessible from Perl scripts.
package
Literally that's all typemap-code which have to be written. All other lines are just policies, i.e. specification/specialization of TypemapObject behaviour.
Let's show xs-adapter code (stored in .xs/.xsi file)
.xs/.xsi
MODULE = MyTest PACKAGE = MyTest::Cookbook::DateRecipe01a # (15) PROTOTYPES: DISABLE DateRecipe01a* DateRecipe01a::new() { RETVAL = new DateRecipe01a(); } # (16) (17) (18) (19) void DateRecipe01a::update() # (20) std::time_t DateRecipe01a::get_epoch() # (21) void DateRecipe01a::DESTROY() { // (22) std::cerr << "xs-adapter MyTest::Cookbook::DateRecipe01a::DESTROY\n"; }
Here and after extended XS::Install's syntax is used.
The standard preambula (15) for xs-adapter should exist.
Let's define constructor for xs-adapter, i.e. for perl class MyTest::Cookbook::DateRecipe01a: (16-19). By usual perl conventions the constructor should be named new, and to let XS-code know that it belongs to MyTest::Cookbook::DateRecipe01a it should be prefixed with DateRecipe01a (17). It returns the newly constructed object (16) of C++ type DateRecipe01a by pointer. The part in braces (18..19) is optional and can be ommited (in that case it will be autogenerated for you).
MyTest::Cookbook::DateRecipe01a
new
The entity (20) is used for mappping perl SV* to the special variable THIS using xs::in.
THIS
xs::in
Lines (20..21) shows how xs-adapter proxies method calls on C++ object if signatures (including parameters) of xs-adapter and C++ class do match.
As the ObjectStorageIV has been chosed as StoragePolicy and Perl takes ownership on the C++ objects to avoid memory leaks, the underlying C++ object (struct DateRecipe01a) have to be deleted when perl wrapper (SV*) is released. To achive that, there is no other way then to hook on DESTROY method of xs-adapter: XS::Framework on XS-parse phase silently inserts Typemap<DateRecipe01a>::destroy() call at the end of DESTROY method of xs-adapter, which is forwarded to TypemapObject::destroy, which forwards to ObjectTypePtr::delete, which actually invokes delete on the object, and no memory leaks occur.
struct DateRecipe01a
DESTROY
TypemapObject::destroy
ObjectTypePtr::delete
delete
The last piece is Makefile.PL, which should look something like
Makefile.PL
use XS::Install; my %params = ( ... CPLUS => 11, SRC => ['src'], INC => '-Isrc', BIN_DEPS => 'XS::Framework', BIN_SHARE => {INCLUDE => {'src' => '/'}}, ); write_makefile(%params);
Please, refer XS::Install documentation for details.
The correct behaviour can be seen in the prove -blv t/cookbook/recipe01.t output:
prove -blv t/cookbook/recipe01.t
t/cookbook/recipe01.t .. # date = MyTest::Cookbook::DateRecipe01a=SCALAR(0x2203578), epoch = 1545299554 # date = MyTest::Cookbook::DateRecipe01a=SCALAR(0x2203578), epoch = 1545299554 ok 1 - no (unexpected) warnings (via done_testing) DateRecipe01a::update() 1..1 xs-adapter MyTest::Cookbook::DateRecipe01a::DESTROY ~DateRecipe01a()
The DESTROY method is executed in Perl eval context, which leads to significant slowdown without any needs (in our case). To avoid that it is recommended to use ObjectStorageMG storage policy. The need of the DESTROY method falls away (in fact, if there will be one, it will lead to slowdown destruction again).
eval
ObjectStorageMG
Here is an reciepe of ObjectStorageMG storage policy. Source class:
// (23) struct DateRecipe01b { DateRecipe01b() { update(false) ; } ~DateRecipe01b() { std::cerr << "~DateRecipe01b()\n"; } void update(bool trace = true) { if (trace) std::cerr << "DateRecipe01b::update()\n"; epoch = std::time(nullptr); } int get_epoch() const { return epoch; } private: std::time_t epoch; };
Typemap for it:
// (24) namespace xs { template <> struct Typemap<DateRecipe01b*> : TypemapObject<DateRecipe01b*, DateRecipe01b*, ObjectTypePtr, ObjectStorageMG, StaticCast> { static std::string package () { return "MyTest::Cookbook::DateRecipe01b"; } }; }
And xs-adapter:
// (25) MODULE = MyTest PACKAGE = MyTest::Cookbook::DateRecipe01b PROTOTYPES: DISABLE DateRecipe01b* DateRecipe01a::new() { RETVAL = new DateRecipe01b(); } void DateRecipe01b::update() std::time_t DateRecipe01b::get_epoch()
The prove -blv t/cookbook/recipe01.t output confirms, that there are no memory leaks:
t/cookbook/recipe01.t .. # date = MyTest::Cookbook::DateRecipe01b=SCALAR(0x221e480), epoch = 1545299554 # date = MyTest::Cookbook::DateRecipe01b=SCALAR(0x221e480), epoch = 1545299554 ok 1 - no (unexpected) warnings (via done_testing) DateRecipe01b::update() 1..1 ~DateRecipe01b()
Please note, that C++ clases DateRecipe01a and DateRecipe01b are identical. In classical XS it is possible to have different aliases and, hence, different typemaps for the same C++ class. As XS::Framework uses C++ template specializations it is not possible to have different specializations for aliases.
DateRecipe01b
To install XS::Framework, copy and paste the appropriate command in to your terminal.
cpanm
cpanm XS::Framework
CPAN shell
perl -MCPAN -e shell install XS::Framework
For more information on module installation, please visit the detailed CPAN module installation guide.