NAME
XS::Framework::Manual::recipe01 - XS::Framework basics
DESCRIPTION
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:
#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.
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).
There is no class inheritance in the current recipe, so basic class DateRecipe01a*
(8) matches final class DateRecipe01a*
(9).
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.
The package
method (13) defines the blessed package name (14) via which the DateRecipe01a
is accessible from Perl scripts.
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)
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).
The entity (20) is used for mappping perl SV* to the special variable THIS
using 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.
The last piece is Makefile.PL
, which should look something like
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:
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).
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.