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.