NAME
XS::Framework::Manual::recipe06 - XS::Framework advanced topic
DESCRIPTION
Let's assume that C++ API offers the following interface:
struct PointRecipe12 {
double x;
double y;
};
struct StatisticsRecipe12 {
StatisticsRecipe12(const PointRecipe12& interest_, const std::vector<PointRecipe12>& points_);
const PointRecipe12& nearest() const;
const PointRecipe12& farest() const;
};
The Statistics
interface is transient, i.e. it's lifetime is linked to the lifetime of it's arguments.: if they are destructed, invoking Statistics
methods will probably lead to crash. The same can be told about returnted result types in the method. This happens when C++ constructor and return types are pointers.
This is quite common C++ pattern of iterator, lazy evaluator etc. However, there is no perlish way to represent this approach as is. One way to handle that is to convert object to functions, i.e. let there be two non-lazy static functions in xs-adapter, one for nearest point and another one for farest point.
In some cases this may be good solution, but in the others it might be suboptimal, e.g. for the cases when all calculations are perfomed on object construction phase, and methods just return values to the precalculated results. Would it be implemented as object converted to functions, the calculation will occur on each method invocation.
Another approach is: create a clone of constructor arguments on a heap, create an transient object instance on a heap and let the SV* wrapper for it holds all required C++ objects. For every returned transient C++ object, let's create another clone and return it to Perl. Having that makes it possible to detach xs-adapter lifetimes from lifetimes of arguments and behave independently; by the price of copying objects, of course. Let's show how to do that.
namespace xs {
template <>
struct Typemap<PointRecipe12*> : TypemapObject<PointRecipe12*, PointRecipe12*, ObjectTypePtr, ObjectStorageMG> {
static std::string package () { return "MyTest::Cookbook::PointRecipe12"; }
};
template <>
struct Typemap<StatisticsRecipe12*> : TypemapObject<StatisticsRecipe12*, StatisticsRecipe12*, ObjectTypePtr, ObjectStorageMG> {
static std::string package () { return "MyTest::Cookbook::StatisticsRecipe12"; }
};
}
MODULE = MyTest PACKAGE = MyTest::Cookbook::PointRecipe12
PROTOTYPES: DISABLE
double PointRecipe12::x(SV* new_val = nullptr) : ALIAS(y = 1) {
double* val = nullptr;
switch(ix) {
case 1: val = &THIS->y; break;
default: val = &THIS->x; break;
}
if (new_val) {
*val = SvNV(new_val);
}
RETVAL = *val;
}
PointRecipe12* PointRecipe12::new(double x = 0, double y = 0) {
RETVAL = new PointRecipe12{x, y};
}
static xs::Sv::payload_marker_t payload_marker_12{}; // (1)
The xs-adapter for Point
and typemaps are quite straighforward. As we are going to use Perl SV* wrapper for Statistics
to store pointers of clones, it is convenient to use magic payload for the purposes (1). For the case will use custom struct (2) to be stored as payload:
struct StatisticsRecipe12_payload { // (2)
PointRecipe12 interest;
std::vector<PointRecipe12> points;
~StatisticsRecipe12_payload() { std::cout << "~StatisticsRecipe12_payload\n"; }
};
static int payload_marker_IntersectionFinderAdder_free(SV*, MAGIC* mg) {
if (mg->mg_virtual == &payload_marker_12) { // (3)
auto* payload = static_cast<StatisticsRecipe12_payload*>((void*)mg->mg_ptr); // (4)
delete payload; // (5)
}
return 0;
}
and because it will be stored as void*
in payload, we need proper destructor for it, i.e. check that it really our payload (3), do convertion from void*
(4) and then finally delete object on a heap (5). Let's write the adapter for Statistics
:
MODULE = MyTest PACKAGE = MyTest::Cookbook::StatisticsRecipe12
PROTOTYPES: DISABLE
PointRecipe12* StatisticsRecipe12::nearest() {
RETVAL = new PointRecipe12(THIS->nearest()); // (6)
}
PointRecipe12* StatisticsRecipe12::farest() {
RETVAL = new PointRecipe12(THIS->farest()); // (7)
}
Sv StatisticsRecipe12::new(PointRecipe12* interest, Array pts) {
using payload_ptr_t = std::unique_ptr<StatisticsRecipe12_payload>;
using stats_ptr_t = std::unique_ptr<StatisticsRecipe12>;
std::vector<PointRecipe12> points; // (8)
for(auto it: pts) {
points.push_back(*(xs::in<PointRecipe12*>(it))); // (9)
}
// better to use std::make_unique from C++ 14, if available. (10)
auto payload_holder = payload_ptr_t{new StatisticsRecipe12_payload{ *interest, std::move(points) }};
auto self_holder = stats_ptr_t{new StatisticsRecipe12{ payload_holder->interest, payload_holder->points }}; // (11)
Object self = xs::out(self_holder.get(), CLASS); // (13)
self.payload_attach((void*)payload_holder.get(), &payload_marker_12); // (14)
// (15)
payload_holder.release();
self_holder.release();
RETVAL = self.ref(); // (16)
}
BOOT {
payload_marker_12.svt_free = payload_marker_IntersectionFinderAdder_free; // (17)
}
To detach found Point
object, we create a copy of th results (6), (7) and return them to Perl. The most complex here is Statistics
construtor. Firtst, the xs::Array
of arguments (probably points?), should be converted to std::vector
of Points (8), as required by C++ interface. The xs::in
method (9) is responsible for the conversion; in case of error (i.e. something non convertible to Point*
has been supplied in Perl), it will throw an exception. Please note, that on line (9) it not only extracts Point*
from Perl array, but also creates a copies of the original points.
On the following lines (10) it acatually creates Startistics
and Statistics payload
objects on a heap. The std::unique_ptr
is used to let the code be exception-safe, e.g. would an exception be thrown at the line (11), the payload_holder
will automatically delete payload on stack unwinding. This is commonly known as RAII principle.
The following code is pretty easy: the SV* wrapper is screated for the pointer held by self_holder
(13), and to the wrapper the payload is attaches as void*
using previously created payload_marker_12
at (1). A soon everything is constructed, the pointer guards are released (15) and the reference to the newly created object is retruned (16). To let Perl know how to delete custom void*
payload, the "deleter" for payload_marker_12
should be attached at XS-module BOOT section (17).
The following code proves the correctness of the chosen approach:
my $stats;
{
$stats = MyTest::Cookbook::StatisticsRecipe12->new(
MyTest::Cookbook::PointRecipe12->new(0.5, 0.5),
[
MyTest::Cookbook::PointRecipe12->new(1, 1),
MyTest::Cookbook::PointRecipe12->new(2, 1),
MyTest::Cookbook::PointRecipe12->new(5, 3),
],
);
};
subtest 'nearest point' => sub {
my $p = $stats->nearest;
is $p->x, 1;
is $p->y, 1;
};
subtest 'farest point' => sub {
my $p = $stats->farest;
is $p->x, 5;
is $p->y, 3;
};
Short summary: for transient C++ objects, which depends on other C++ objects without owning them, sometimes it is possible to create a copy of them on a heap, and let Perl SV* wrapper holds them. That's way "safe" Perl interface might be designed.