—=head1 NAME
XS::Framework::Manual::recipe01 - XS::Framework basics
=cut
=head1 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 B<typemap> for C<DateRecipe01a>. It is
recommended to have an external C<.h> file, however for the bravety
it is embedded into C<.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 L<XS::Framework> C++ headers should be included as shown in (1). Then the
target library headers should be included (2).
Then the C<typemap> (aka Perl-C++ mapping rules for particular class) should be
defined: it is done as C<Typemap> class (5) full specialization (4) in the
C<xs> namespace (3). This is similar how hash-function is for
C<std::unordererd_hash> is specialized in namespace C<std> for user supplied
classes.
We decided to use B<pointer-semantics> (6), i.e. create in C++ code the
C<DateRecipe01a> objects on heap and return them to Perl. As specializing
C<Typemap> from scratch is non-trivial job, it is recommended to use
the supplied helper C<TypemapObject> (7).
There is no class inheritance in the current recipe, so basic class
C<DateRecipe01a*> (8) matches final class C<DateRecipe01a*> (9).
We decided to follow commonly used pattern in CPAN-modules, i.e. C<LifetimePolicy>
= C<ObjectTypePtr> (10), which transfers object ownernership to Perl via pointer
and invoke C<delete DateRecipe01a*> when perl wrapper goes out of life; and
C<StoragePolicy> = C<ObjectStorageIV> (11) to use integer field of perl SV*
to store pointer to C<DateRecipe01a*>. As the C++ class <DateRecipe01a*> does
not participates in virtual inheritance in accordance with C++ rules it is
possible to use C<static_cast>(12) policy for pointer; it is the fastest
approach as it has zero runtime costs.
The C<package> method (13) defines the blessed package name (14) via which
the C<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 C<TypemapObject>
behaviour.
Let's show xs-adapter code (stored in C<.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 L<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
C<MyTest::Cookbook::DateRecipe01a>: (16-19). By usual perl conventions
the constructor should be named C<new>, and to let XS-code know that it
belongs to C<MyTest::Cookbook::DateRecipe01a> it should be prefixed
with C<DateRecipe01a> (17). It returns the newly constructed object (16)
of C++ type C<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 C<THIS> using C<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 C<ObjectStorageIV> has been chosed as C<StoragePolicy> and Perl
takes ownership on the C++ objects to avoid memory leaks, the underlying
C++ object (C<struct DateRecipe01a>) have to be deleted when perl wrapper
(SV*) is released. To achive that, there is no other way then to hook
on C<DESTROY> method of xs-adapter: L<XS::Framework> on XS-parse phase
silently inserts Typemap<DateRecipe01a>::destroy() call at the end of
C<DESTROY> method of xs-adapter, which is forwarded to
C<TypemapObject::destroy>, which forwards to C<ObjectTypePtr::delete>,
which actually invokes C<delete> on the object, and no memory leaks
occur.
The last piece is C<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 L<XS::Install> documentation for details.
The correct behaviour can be seen in the C<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 C<DESTROY> method is executed in Perl C<eval> context, which leads to
significant slowdown without any needs (in our case). To avoid that it
is recommended to use C<ObjectStorageMG> storage policy. The need of
the C<DESTROY> method falls away (in fact, if there will be one, it will
lead to slowdown destruction again).
Here is an reciepe of C<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 C<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 C<DateRecipe01a> and C<DateRecipe01b> are identical.
In classical XS it is possible to have different aliases and, hence, different
typemaps for the same C++ class. As L<XS::Framework> uses C++ template
specializations it is not possible to have different specializations for
I<aliases>.
=cut