PP(1p)
π PP(1p) β the PDL::PP era π
User Contributed Perl Documentation Β· PP(1p)
β¨ NAME β¨
PDL::PP β generate PDL routines from concise descriptions. bestie, you write a lil description, it writes you a whole C function. it's giving β¨manifestationβ¨.
π SYNOPSIS π
okay so first things first, let PDL::PP yap about what it's up to:
# let PDL::PP tell you what it's doing
$::PP_VERBOSE = 1;
pp_def(
'sumover',
Pars => 'a(n); [o]b();',
Code => q{
double tmp=0;
loop(n) %{
tmp += $a();
%}
$b() = tmp;
},
);
pp_done();
# do not call exit() as some processing can be done in same process
π«Ά do NOT call exit() β some processing can happen in the same process, so bailing early is lowkey rude to the rest of your build. periodt.
π FUNCTIONS π
here's the quick lil cheat sheet of everything PDL::PP hands you. no gatekeeping here π
πΈ pp_add_boot
add code to the BOOT section of the generated XS file. cute lil setup spot.
π¦ pp_add_exported
add functions to the list of exported functions. sharing is caring.
π pp_add_isa
add entries to the @ISA list. family tree business.
π pp_addbegin
sets code to be added at the top of the generated .pm file.
π pp_addhdr
add code and includes to the C section of the generated XS file.
okay so listen. when you're in a module that's "multi-C" (aka one .c file per pp_defed function), you gotta keep in mind that as each one gets generated, all the pp_addhdr calls so far get included. so if you're adding C functions, make them static so they don't clash with the later .c files. π
but honestly? a better practice is making them separate C files, with any necessary .h that both they and the .pd file include. then you can add them to your Makefile.PL. note this is the _int version, so peep the separate notes on how to "opt-in" for your own modules:
my @pack = (["pnm.pd", qw(Pnm PDL::IO::Pnm)]);
my %hash = pdlpp_stdargs_int(@pack);
$hash{OBJECT} .= ' get$(OBJ_EXT)';
sub MY::postamble { pdlpp_postamble_int(@pack); }
WriteMakefile(%hash);
π pp_addpm
add code to the generated .pm file.
β¨ pp_addxs
add extra XS code to the generated XS file. extra credit energy.
π pp_add_macros
add extra $MACRO() definitions for these functions. note these generate C code. as of 2.080, they get passed the list of arguments they were called with, rather than a single string β split like the C pre-processor splits on commas, except if you're inside "" or (), and with leading and trailing whitespace removed. π§
pp_add_macros(SUCC => sub { "($_[0] + 1)" });
# ...
Code => '$a() = $SUCC($b());',
π¦ pp_add_typemaps
available from 2.082. add an XS typemap for use as OtherPars or from manually-added XS. takes one named argument, either typemap (an ExtUtils::Typemaps object), string, or file. π
pp_add_typemaps(string=><<'EOT');
TYPEMAP: <<END_OF_TYPEMAP
TYPEMAP
NV_ADD1 T_NV_ADD1
INPUT
T_NV_ADD1
$var = SvNV($arg) + 1;
OUTPUT
T_NV_ADD1
sv_setnv($arg, $var - 1);
END_OF_TYPEMAP
EOT
# ...
OtherPars => '[o] NV_ADD1 v1',
π pp_beginwrap
add BEGIN-block wrapping to code for the generated .pm file. wrap it up, literally.
π pp_bless
sets the package to which the XS code is added β default is PDL.
πΈ pp_core_importList
specify what is imported from PDL::Core.
β pp_def
define a new PDL function. this is THE main character, fr fr.
π pp_deprecate_module
add runtime and POD warnings about a module being deprecated. rip to that module, respectfully.
π pp_done
mark the end of PDL::PP definitions in the file. the closing credits.
π«Ά pp_export_nothing
clear out the export list for your generated module. no thoughts, empty export list.
π§ pp_line_numbers
add line number information to simplify debugging of PDL::PP code. your future self says thank you.
π that's the vibe check for part 1, more comingβ¦
π¦ OVERVIEW π¦
okay so if you want a whole other intro to PDL::PP, bestie, there's this thing called Practical Magick with C, PDL, and PDL::PP -- a guide to compiled add-ons for PDL over at https://arxiv.org/abs/1702.07753. go read it, it's giving β¨scholarlyβ¨.
so like. why do we even need PP? several reasons, and I'm gonna list them:
firstly β we want to be able to generate subroutine code for each of the PDL datatypes (PDL_Byte, PDL_Short, etc). AUTOMATICALLY. no cap. not me hand-writing that.
secondly β when you're referring to slices of PDL arrays in Perl (like $x->slice('0:10:2,:') or other things such as transposes), it's genuinely nice to be able to do this transparently, and to be able to do this 'in-place' β i.e., not to have to make a memory copy of the section. PP handles all the necessary element and offset arithmetic for you, bestie. it's literally doing the math so you don't have to π«Ά. there are also the notions of broadcasting (repeated calling of the same routine for multiple slices, see PDL::Indexing) and dataflow (see PDL::Dataflow, and "DefaultFlow") which use of PP allows.
in much of what follows we're gonna assume you already know the concepts of implicit and explicit broadcasting and index manipulations within PDL. if you have not yet heard of these concepts or are not very comfortable with them, it is time to check PDL::Indexing. that's your homework, slay. π
as you may appreciate from its name, PDL::PP is a Pre-Processor β i.e. it expands code via substitutions to make real C-code. technically the output is XS code (see perlxs) but that is very close to C. iykyk.
so how do you use PP? well for the most part you just write ordinary C code, except for special PP constructs which take the form:
$something(something else)
or:
PPfunction %{
<stuff>
%}
the most important PP construct is the form $array(). consider the very simple PP function to sum the elements of a 1D vector (in fact this is very similar to the actual code used by 'sumover'):
pp_def('sumit',
Pars => 'a(n); [o]b();',
Code => q{
double tmp;
tmp = 0;
loop(n) %{
tmp += $a();
%}
$b() = tmp;
}
);
what's going on?? π okay so the "Pars =>" line is very important for PP β it specifies all the arguments and their dimensionality. we call this the signature of the PP function (compare also the explanations in PDL::Indexing). in this case the routine takes a 1-D function as input and returns a 0-D scalar as output. the $a() PP construct is used to access elements of the array a(n) for you β PP fills in all the required C code. icon behaviour, honestly.
you will notice that we are using the "q{}" single-quote operator. this is not an accident. you generally want to use single quotes to denote your PP Code sections. PDL::PP uses $var() for its parsing and if you don't use single quotes, Perl will try to interpolate $var(). also, using the single quote "q" operator with curly braces makes it look like you are creating a code block, which is What You Mean. (Perl is smart enough to look for nested curly braces and not close the quote until it finds the matching curly brace, so it's safe to have nested blocks.) under other circumstances, such as when you're stitching together a Code block using string concatenations, it's often easiest to use real single quotes as
Code => 'something'.$interpolatable.'somethingelse;'
in the simple case here where all elements are accessed, the PP construct "loop(n) %{ ... %}" is used to loop over all elements in dimension "n". note this feature of PP: ALL DIMENSIONS ARE SPECIFIED BY NAME.
this is made clearer if we avoid the PP loop() construct and write the loop explicitly using conventional C:
pp_def('sumit',
Pars => 'a(n); [o]b();',
Code => q{
PDL_Indx i,n_size;
double tmp;
n_size = $SIZE(n);
tmp = 0;
for(i=0; i<n_size; i++) {
tmp += $a(n=>i);
}
$b() = tmp;
},
);
which does the same as before, but is more long-winded. you can see that to get element "i" of a() we say $a(n=>i) β we are specifying the dimension by name "n". in 2D we might say:
Pars=>'a(m,n);',
...
tmp += $a(m=>i,n=>j);
...
the syntax "m=>i" borrows from Perl hashes, which are in fact used in the implementation of PP. one could also say "$a(n=>j,m=>i)" as order is not important. π
you can also see in the above example the use of another PP construct β $SIZE(n) to get the length of the dimension "n".
it should, however, be noted that you shouldn't write an explicit C-loop when you could have used the PP "loop" construct, since PDL::PP checks automatically the loop limits for you, usage of "loop" makes the code more concise, etc. but there are certainly situations where you need explicit control of the loop and now you know how to do it ;). π
to revisit 'Why PP?' β the above code for sumit() will be generated for each data-type. it will operate on slices of arrays 'in-place'. it will broadcast automatically β e.g. if a 2D array is given it will be called repeatedly for each 1D row (again check PDL::Indexing for the details of broadcasting). and then b() will be a 1D array of sums of each row. we could call it with $x->transpose to sum the columns instead. and Dataflow tracing etc. will be available. obsessed. πΈ
you can see PP saves the programmer from writing a lot of needlessly repetitive C-code β in our opinion this is one of the best features of PDL, making writing new C subroutines for PDL an amazingly concise exercise. a second reason is the ability to make PP expand your concise code definitions into different C code based on the needs of the computer architecture in question. imagine for example you are lucky to have a supercomputer at your hands; in that case you want PDL::PP certainly to generate code that takes advantage of the vectorising/parallel computing features of your machine (this a project for the future). in any case, the bottom line is that your unchanged code should still expand to working XS code even if the internals of PDL changed. π
also, because you are generating the code in an actual Perl script, there are many fun things that you can do. let's say that you need to write both sumit (as above) and multit. with a little bit of creativity, we can do
for({Name => 'sumit', Init => '0', Op => '+='},
{Name => 'multit', Init => '1', Op => '*='}) {
pp_def($_->{Name},
Pars => 'a(n); [o]b();',
Code => '
double tmp;
tmp = '.$_->{Init}.';
loop(n) %{
tmp '.$_->{Op}.' $a();
%}
$b() = tmp;
');
}
which defines both the functions easily. now, if you later need to change the signature or dimensionality or whatever, you only need to change one place in your code. yeah, sure, your editor does have 'cut and paste' and 'search and replace', but it's still less bothersome and definitely more difficult to forget just one place and have strange bugs creep in. π also, adding 'orit' (bitwise or) later is a one-liner. we love to see it.
and remember, you really have Perl's full abilities with you β you can very easily read any input file and make routines from the information in that file. for simple cases like the above, the author (Tjl) currently favors the hash syntax like the above β it's not too much more characters than the corresponding array syntax but much easier to understand and change. π§
as of 2.064, the "Code" must not just "return", since the signature of the generated functions has changed from returning "void" to returning a "pdl_error", which is pre-initialised to a successful return value. you can easily just replace the "return;" with "return PDL_err;", which is the variable's name. noted, bestie, don't forget it. π
we should mention here also the ability to get the pointer to the beginning of the data in memory β a prerequisite for interfacing PDL to some libraries. this is handled with the $P(var) directive, see below. π
when starting work on a new pp_def'ined function, if you make a mistake, you will usually find a pile of compiler errors indicating line numbers in the generated XS file. if you know how to read XS files (or if you want to learn the hard way), you could open the generated XS file and search for the line number with the error. however, a recent addition to PDL::PP helps report the correct line number of your errors: "pp_line_numbers". working with the original summit example, if you had a mis-spelling of tmp in your code, you could change the (erroneous) code to something like this and the compiler would give you much more useful information:
pp_def('sumit',
Pars => 'a(n); [o]b();',
Code => pp_line_numbers(__LINE__, q{
double tmp;
tmp = 0;
loop(n) %{
tmp += $a();
%}
$b() = rmp;
})
);
for the above situation, my compiler tells me:
...
test.pd:15: error: 'rmp' undeclared (first use in this function)
...
in my example script (called test.pd), line 15 is exactly the line at which I made my typo: "rmp" instead of "tmp". the way that actually saves you ππ₯
so, after this quick overview of the general flavour of programming PDL routines using PDL::PP, let's summarise in which circumstances you should actually use this preprocessor/precompiler. you should use PDL::PP if you want to
- interface PDL to some external library
- write some algorithm that would be slow if coded in Perl (this is not as often as you think; take a look at broadcasting and dataflow first).
- be a PDL developer (and even then it's not obligatory)
π WARNING π
because of its architecture, PDL::PP can be both flexible and easy to use on the one hand, yet exuberantly complicated at the same time. currently, part of the problem is that error messages are not very informative, and if something goes wrong, you'd better know what you are doing and be able to hack your way through the internals (or be able to figure out by trial and error what is wrong with your args to "pp_def"). this is real, bestie, prepare yourself. although work is being done to produce better warnings, do not be afraid to send your questions to the mailing list if you run into trouble. π«Ά we've all been there.
β¨ DESCRIPTION β¨
okay so bestie, now that you have some idea how to use pp_def to define new PDL functions, it's time to spill the tea on the general syntax of pp_def. π« pp_def takes as arguments, first, the name of the function you're defining, and then a hash list that can contain various keys.
Based on these keys, PP generates XS code and a .pm file. The function pp_done (see the example in the SYNOPSIS) is how you tell PDL::PP that there are no more definitions in this file and it's time to generate the .xs and .pm file. β¨
As a consequence, there may be several pp_def() calls inside a file (by convention, files with PP code have the extension .pd or .pp) but generally only one pp_done().
There are two main different types of usage of pp_def(): the 'data operation' and the 'slice operation' prototypes.
The 'data operation' is used to take some data, mangle it, and output some other data β this includes, for example, the + operation, matrix inverse, sumover, etc., and all the examples we've talked about in this document so far. Implicit and explicit broadcasting and the creation of the result are taken care of automatically in those operations. You can even do dataflow with sumit, sumover, etc. (don't be dismayed if you don't understand the concept of dataflow in PDL very well yet; it's still very much experimental, fr fr).
The 'slice operation' is a different kind of operation: in a slice operation, you're not changing any data, you're defining correspondences between different elements of two ndarrays (examples include the index manipulation/slicing function definitions in the file slices.pd that is part of the PDL distribution; but beware, this is not introductory level stuff π).
To support bad values, additional keys are required for pp_def, as explained below.
If you're just interested in communicating with some external library (for example some linear algebra/matrix library), you'll usually want the 'data operation', so we're going to discuss that first. π
π₯ DATA OPERATION π₯
πΈ A simple example πΈ
In the data operation, you must know what dimensions of data you need. First, an example with scalars:
pp_def('add',
Pars => 'a(); b(); [o]c();',
Code => '$c() = $a() + $b();'
);
That looks a little strange, but let's dissect it, bestie. π§ The first line is easy: we're defining a routine with the name add. The second line simply declares our parameters, and the parentheses mean that they're scalars. We call the string that defines our parameters and their dimensionality the signature of that function. For its relevance with regard to broadcasting and index manipulations, check the PDL::Indexing man page.
The third line is the actual operation. You need to use the dollar signs and parentheses to refer to your parameters (this will probably change at some point in the future, once a good syntax is found).
These lines are all that's necessary to actually define the function for PDL (well, actually it isn't; you additionally need to write a Makefile.PL (see below) and build the module (something like perl Makefile.PL; make); but let's ignore that for the moment). So now you can do
use MyModule;
$x = pdl 2,3,4;
$y = pdl 5;
$c = add($x,$y);
# or
add($x,$y,($c=null)); # Alternative form, useful if $c has been
# preset to something big, not useful here.
and have broadcasting work correctly (the result is $c == [7 8 9]). slay π
π The Pars section: the signature of a PP function π
Seeing the above example code, you'll most probably ask: what is this strange $c=null syntax in the second call to our new add function? If you take another look at the definition of add, you'll notice that the third argument c is flagged with the qualifier [o], which tells PDL::PP that this is an output argument. So the above call to add means 'create a new $c from scratch with correct dimensions' β null is a special token for 'empty ndarray' (you might ask why we haven't used the value undef to flag this instead of the PDL specific null; we're currently thinking about it ;).
[This should be explained in some other section of the manual as well!!]
The reason for having this syntax as an alternative is that if you have really huge ndarrays, you can do
$c = PDL->null;
for(some long loop) {
# munge a,b
add($x,$y,$c);
# munge c, put something back to x,y
}
and avoid allocating and deallocating $c each time. It's allocated once at the first add() and thereafter the memory stays until $c is destroyed. πΎ
If you just say
$c = add($x,$y);
the code generated by PP will automatically fill in $c=null and return the result. If you want to learn more about the reasons why PDL::PP supports this style, where output arguments are given as last arguments, check the PDL::Indexing man page.
[o] is not the only qualifier a pdl argument can have in the signature. Another important qualifier is the [t] option, which flags a pdl as temporary. What does that mean? You tell PDL::PP that this pdl is only used for temporary results in the course of the calculation, and you're not interested in its value after the computation has been completed. But why should PDL::PP want to know about this in the first place? The reason is closely related to the concepts of pdl auto creation (you heard about that above) and implicit broadcasting. If you use implicit broadcasting, the dimensionality of automatically created pdls is actually larger than that specified in the signature. With [o] flagged pdls, they'll be created so that they have the additional dimensions as required by the number of implicit broadcast dimensions. When creating a temporary pdl, however, it'll always only be made big enough so that it can hold the result for one iteration in a broadcast loop, i.e. as large as required by the signature. So less memory is wasted when you flag a pdl as temporary. Secondly, you can use output auto creation with temporary pdls even when you're using explicit broadcasting, which is forbidden for normal output pdls flagged with [o] (see PDL::Indexing).
As of 2.073, the user is unable to pass a [t] parameter, and PDL will create and size it to its notional size, times the number of threads.
Here's an example where we use the [t] qualifier. We define the function callf that calls a C routine f which needs a temporary array of the same size and type as the array a (sorry about the forward reference for $P; it's a pointer access, see below) :
pp_def('callf',
Pars => 'a(n); [t] tmp(n); [o] b()',
Code => 'PDL_Indx ns = $SIZE(n);
f($P(a),$P(b),$P(tmp),ns);
'
);
Another possible qualifier is [phys]. If given, this means the pdl will have make_physical in PDL::Core called on it.
Additionally, if it has a specified dimension d that has value 1, d will not magically be grown if d is larger in another pdl with specified dimension d, and instead an exception will be thrown. E.g.:
pp_def('callf',
Pars => 'a(n); [phys] b(n); [o] c()',
# ...
);
If a had lead dimension of 2 and b of 3, an exception will always be thrown. However, if b has lead dimension of 1, it would be silently repeated as if it were 2, if it were not a phys parameter. π€
π¦ Argument dimensions and the signature π¦
Now we've just talked about dimensions of pdls and the signature. How are they related? Let's say that we want to add a scalar + the index number to a vector:
pp_def('add2',
Pars => 'a(n); b(); [o]c(n);',
Code => 'loop(n) %{
$c() = $a() + $b() + n;
%}'
);
There are several points to notice here: first, the Pars argument now contains the n arguments to show that we have a single dimensions in a and c. It's important to note that dimensions are actual entities that are accessed by name, so this declares a and c to have the same first dimensions. In most PP definitions the size of named dimensions will be set from the respective dimensions of non-output pdls (those with no [o] flag) but sometimes you might want to set the size of a named dimension explicitly through an integer parameter. See below in the description of the OtherPars section how that works. π
π Constant argument dimensions in the signature π
Suppose you want an output ndarray to be created automatically and you know that on every call its dimension will have the same size (say 9) regardless of the dimensions of the input ndarrays. In this case you use the following syntax in the Pars section to specify the size of the dimension:
' [o] y(n=9); '
As expected, extra dimensions required by broadcasting will be created if necessary. If you need to assign a named dimension according to a more complicated formula (than a constant) you must use the RedoDimsCode key described below.
π Type conversions and the signature π
The signature also determines the type conversions that will be performed when a PP function is invoked. So what happens when we invoke one of our previously defined functions with pdls of different type, e.g.
add2($x,$y,($ret=null));
where $x is of type PDL_Float and $y of type PDL_Short? With the signature as shown in the definition of add2 above, the datatype of the operation (as determined at runtime) is that of the pdl with the 'highest' type (sequence is byte < short < ushort < long < float < double). In the add2 example the datatype of the operation is float ($x has that datatype). All pdl arguments are then type converted to that datatype (they are not converted inplace but a copy with the right type is created if a pdl argument doesn't have the type of the operation). Null pdls don't contribute a type in the determination of the type of the operation. However, they will be created with the datatype of the operation; here, for example, $ret will be of type float. You should be aware of these rules when calling PP functions with pdls of different types, to take the additional storage and runtime requirements into account. π§ β¨
Okay so these type conversions? They're correct for most functions you'd normally define with pp_def. π But bestie, there are certain cases where you want slightly modified type conversion behaviour. For those, you can use additional qualifiers in the signature to specify the desired properties with regard to type conversion. These qualifiers can be combined with the ones we already met (the creation qualifiers [o] and [t]). Let's go through the list of qualifiers that change type conversion behaviour. β¨
The most important is the indx qualifier, which comes in handy when a pdl argument represents indices into another pdl. Let's take a look at an example from PDL::Ufunc:
pp_def('maximum_ind',
Pars => 'a(n); indx [o] b()',
Code => '$GENERIC() cur;
PDL_Indx curind;
loop(n) %{
if (!n || $a() > cur) {cur = $a(); curind = n;}
%}
$b() = curind;',
);
The function maximum_ind finds the index of the largest element of a vector. If you look at the signature you notice that the output argument b has been declared with the additional indx qualifier. This has the following consequences for type conversions: regardless of the type of the input pdl a the output pdl b will be of type PDL_Indx which makes sense since b will represent an index into a. π
Note that curind is declared as type PDL_Indx and not indx. While most datatype declarations in the Pars section use the same name as the underlying C type, indx is a type which is sufficient to handle PDL indexing operations. For 32-bit installs, it can be a 32-bit integer type. For 64-bit installs, it will be a 64-bit integer type. π
Furthermore, if you call the function with an existing output pdl b its type will not influence the datatype of the operation (see above). Hence, even if a is of a smaller type than b it will not be converted to match the type of b but stays untouched, which saves memory and CPU cycles and is the right thing to do when b represents indices. Also note that you can use the indx qualifier together with other qualifiers (the [o] and [t] qualifiers). Order is significant -- type qualifiers precede creation qualifiers ([o] and [t]). π
The above example also demonstrates typical usage of the $GENERIC() macro. It expands to the current type in a so called generic loop. What is a generic loop? As you already heard a PP function has a runtime datatype as determined by the type of the pdl arguments it has been invoked with. The PP generated XS code for this function therefore contains a switch like switch (type) {case PDL_Byte: ... case PDL_Double: ...} that selects a case based on the runtime datatype of the function (it's called a type ``loop'' because there is a loop in PP code that generates the cases). In any case your code is inserted once for each PDL type into this switch statement. The $GENERIC() macro just expands to the respective type in each copy of your parsed code in this switch statement, e.g., in the case PDL_Byte section cur will expand to PDL_Byte and so on for the other case statements. I guess you realise that this is a useful macro to hold values of pdls in some code. π
There are a couple of other qualifiers with similar effects as indx. For your convenience there are the float and double qualifiers with analogous consequences on type conversions as indx. Let's assume you have a very large array for which you want to compute row and column sums with an equivalent of the sumover function. However, with the normal definition of sumover you might run into problems when your data is, e.g. of type short. A call like
sumover($large_pdl,($sums = null));
will result in $sums be of type short and is therefore prone to overflow errors if $large_pdl is a very large array. On the other hand calling
@dims = $large_pdl->dims; shift @dims;
sumover($large_pdl,($sums = zeroes(double,@dims)));
is not a good alternative either. Now we don't have overflow problems with $sums but at the expense of a type conversion of $large_pdl to double, something bad if this is really a large pdl. That's where double comes in handy:
pp_def('sumoverd',
Pars => 'a(n); double [o] b()',
Code => 'double tmp=0;
loop(n) %{ tmp += a(); %}
$b() = tmp;',
);
This gets us around the type conversion and overflow problems. Again, analogous to the indx qualifier double results in b always being of type double regardless of the type of a without leading to a type conversion of a as a side effect. π«Ά
There is also a special type, real. The others above are all actual PDL/C datatypes, but real is a modifier; if the operation type is real, it has no effect; if it is complex, then the parameter will be the real version - so cdouble becomes double, etc. π
There is also the converse, complex. If the operation is already complex, there is no effect; if not, the output will be promoted to the type's complexversion in PDL::Type, which defaults to cfloat. Note this is controlled both by the PDL::Types data, and the code in PDL::PP. NB Because this outputs floating-point data, the inputs will by definition be turned into such. Therefore, it only makes sense to have floating-point GenericTypes inputs. If you want to default to coercing inputs to float, give that as the last GenericTypes as the generated XS function defaults to the last-given one. Hence (with the PMCode and Doc omitted):
pp_def('r2C',
GenericTypes=>[reverse qw(F D G C)], # last one is default so here = F
Pars => 'r(); complex [o]c()',
Code => '$c() = $r();'
);
Finally, there are the type+ qualifiers where type is one of int or float. What shall that mean. Let's illustrate the int+ qualifier with the actual definition of sumover:
pp_def('sumover',
Pars => 'a(n); int+ [o] b()',
Code => '$GENERIC(b) tmp=0;
loop(n) %{ tmp += a(); %}
$b() = tmp;',
);
As we had already seen for the int, float and double qualifiers, a pdl marked with a type+ qualifier does not influence the datatype of the pdl operation. Its meaning is "make this pdl at least of type type or higher, as required by the type of the operation". In the sumover example this means that when you call the function with an a of type PDL_Short the output pdl will be of type PDL_Long (just as would have been the case with the int qualifier). This again tries to avoid overflow problems when using small datatypes (e.g. byte images). However, when the datatype of the operation is higher than the type specified in the type+ qualifier b will be created with the datatype of the operation, e.g. when a is of type double then b will be double as well. We hope you agree that this is sensible behaviour for sumover. It should be obvious how the float+ qualifier works by analogy. It may become necessary to be able to specify a set of alternative types for the parameters. However, this will probably not be implemented until someone comes up with a reasonable use for it. π
Note that we now had to specify the $GENERIC macro with the name of the pdl to derive the type from that argument. Why is that? If you carefully followed our explanations you will have realised that in some cases b will have a different type than the type of the operation. Calling the $GENERIC macro with b as argument makes sure that the type will always the same as that of b in that part of the generic loop. π
This is about all there is to say about the Pars section in a pp_def call. You should remember that this section defines the signature of a PP defined function, you can use several options to qualify certain arguments as output and temporary args and all dimensions that you can later refer to in the Code section are defined by name.
It is important that you understand the meaning of the signature since in the latest PDL versions you can use it to define broadcasting functions from within Perl, i.e. what we call Perl level broadcasting. Please check PDL::Indexing for details.
The Code section π»β¨
The Code section contains the actual XS code that will be in the innermost part of a broadcast loop (if you don't know what a broadcast loop is then you still haven't read PDL::Indexing; do it now ;) after any PP macros (like $GENERIC) and PP functions have been expanded (like the loop function we are going to explain next).
Let's quickly reiterate the sumover example:
pp_def('sumover',
Pars => 'a(n); int+ [o] b()',
Code => '$GENERIC(b) tmp=0;
loop(n) %{ tmp += a(); %}
$b() = tmp;',
);
The loop construct in the Code section also refers to the dimension name so you don't need to specify any limits: the loop is correctly sized and everything is done for you, again. β¨
Next, there is the surprising fact that $a() and $b() do not contain the index. This is not necessary because we're looping over n and both variables know which dimensions they have so they automatically know they're being looped over. π
This feature comes in very handy in many places and makes for much shorter code. Of course, there are times when you want to circumvent this; here is a function which make a matrix symmetric and serves as an example of how to code explicit looping:
pp_def('symm',
Pars => 'a(n,n); [o]c(n,n);',
Code => 'loop(n) %{
int n2;
for(n2=n; n2<$SIZE(n); n2++) {
$c(n0 => n, n1 => n2) =
$c(n0 => n2, n1 => n) =
$a(n0 => n, n1 => n2);
}
%}
'
);
okay so let's dissect what is happening bestie π
Firstly, what is this function supposed to do? From its signature you see that it takes a 2D matrix with equal numbers of columns and rows and outputs a matrix of the same size. From a given input matrix $a it computes a symmetric output matrix $c (symmetric in the matrix sense that A^T = A where ^T means matrix transpose, or in PDL parlance $c == $c->transpose). It does this by using only the values on and below the diagonal of $a. In the output matrix $c all values on and below the diagonal are the same as those in $a while those above the diagonal are a mirror image of those below the diagonal (above and below are here interpreted in the way that PDL prints 2D pdls). If this explanation still sounds a bit strange just go ahead, make a little file into which you write this definition, build the new PDL extension (see section on Makefiles for PP code) and try it out with a couple of examples.
Having explained what the function is supposed to do there are a couple of points worth noting from the syntactical point of view. First, we get the size of the dimension named "n" again by using the $SIZE macro. Second, there are suddenly these funny "n0" and "n1" index names in the code though the signature defines only the dimension "n". Why this? The reason becomes clear when you note that both the first and second dimension of $a and $b are named "n" in the signature of "symm". This tells PDL::PP that the first and second dimension of these arguments should have the same size. Otherwise the generated function will raise a runtime error. However, now in an access to $a and $c PDL::PP cannot figure out which index "n" refers to any more just from the name of the index. Therefore, the indices with equal dimension names get numbered from left to right starting at 0, e.g. in the above example "n0" refers to the first dimension of $a and $c, "n1" to the second and so on.
In all examples so far, we have only used the "Pars" and "Code" members of the hash that was passed to "pp_def". There are certainly other keys that are recognised by PDL::PP and we will hear about some of them in the course of this document. Find a (non-exhaustive) list of keys in Appendix A. A list of macros and PPfunctions (we have only encountered some of those in the examples above yet) that are expanded in values of the hash argument to "pp_def" is summarised in Appendix B.
At this point, it might be appropriate to mention that PDL::PP is not a completely static, well designed set of routines (as Tuomas puts it: "stop thinking of PP as a set of routines carved in stone") but rather a collection of things that the PDL::PP author (Tuomas J. Lukka) considered he would have to write often into his PDL extension routines. PP tries to be expandable so that in the future, as new needs arise, new common code can be abstracted back into it. If you want to learn more on why you might want to change PDL::PP and how to do it check the section on PDL::PP internals.
β¨ Handling bad values β¨
There are several keys and macros used when writing code to handle bad values. The first one is the "HandleBad" key:
HandleBad => 0β This flags a pp-routine as NOT handling bad values. If this routine is sent ndarrays with their"badflag"set, then a warning message is printed toSTDOUTand the ndarrays are processed as if the value used to represent bad values is a valid number. The"badflag"value is not propagated to the output ndarrays.
An example of when this is used is for FFT routines, which generally do not have a way of ignoring part of the data.
HandleBad => 1β This causes PDL::PP to write extra code that ensures theBadCodesection is used, and that the$ISBAD()macro (and its brethren) work. If no"BadCode"is supplied, the"Code"section will be used, on the assumption it will use"PDL_IF_BAD"to handle bad values.HandleBadis not given β If any of the input ndarrays have their"badflag"set, then the output ndarrays will have their"badflag"set, but any suppliedBadCodeis ignored.
The value of "HandleBad" is used to define the contents of the "BadDoc" key, if it is not given.
To handle bad values, code must be written somewhat differently; for instance,
$c() = $a() + $b();
becomes something like
if ( $a() != BADVAL && $b() != BADVAL ) {
$c() = $a() + $b();
} else {
$c() = BADVAL;
}
However, we only want the second version if bad values are present in the input ndarrays (and that bad-value support is wanted!) - otherwise we actually want the original code. This is where the "BadCode" key comes in; you use it to specify the code to execute if bad values may be present, and PP uses both it and the "Code" section to create something like:
if ( bad_values_are_present ) {
fancy_broadcastloop_stuff {
BadCode
}
} else {
fancy_broadcastloop_stuff {
Code
}
}
This approach means that there is virtually no overhead when bad values are not present (i.e. the badflag routine returns 0).
The C preprocessor symbol "PDL_BAD_CODE" is defined when the bad code is compiled, so that you can reduce the amount of code you write. The BadCode section can use the same macros and looping constructs as the Code section. As of 2.073, you can also use "PDL_IF_BAD(iftrue,iffalse)".
π Other bad-value macros π
However, it wouldn't be much use without the following additional macros:
$ISBAD(var)β To check whether an ndarray's value is bad, use the$ISBADmacro:
if ( $ISBAD(a()) ) { printf("a() is bad\n"); }
You can also access given elements of an ndarray:
if ( $ISBAD(a(n=>l)) ) { printf("element %d of a() is bad\n", l); }
$ISGOOD(var)β This is the opposite of the$ISBADmacro.$SETBAD(var)β For when you want to set an element of an ndarray bad.$ISBADVAR(c_var,pdl)β If you have cached the value of an ndarray$a()into a c-variable ("foo"say), then to check whether it is bad, use"$ISBADVAR(foo,a)".$ISGOODVAR(c_var,pdl)β As above, but this time checking that the cached value isn't bad.$SETBADVAR(c_var,pdl)β To copy the bad value for an ndarray into a c variable, use"$SETBADVAR(foo,a)".
TODO: mention $PPISBAD() etc macros.
π PDL STATE macros π
If you want access to the value of the badflag for a given ndarray, you can use the PDL STATE macros, for use in "CopyBadStatusCode" and "FindBadStatusCode".
$ISPDLSTATEBAD(pdl)$ISPDLSTATEGOOD(pdl)$SETPDLSTATEBAD(pdl)$SETPDLSTATEGOOD(pdl)
And for use in "Code" sections:
$PDLSTATEISBAD(pdl)$PDLSTATEISGOOD(pdl)$PDLSTATESETBAD(pdl)$PDLSTATESETGOOD(pdl)
π¦ Bad-value examples π¦
Using these macros, the above code could be specified as:
Code => '$c() = $a() + $b();',
BadCode => '
if ( $ISBAD(a()) || $ISBAD(b()) ) {
$SETBAD(c());
} else {
$c() = $a() + $b();
}',
Since this is Perl, TMTOWTDI, so you could also write:
BadCode => '
if ( $ISGOOD(a()) && $ISGOOD(b()) ) {
$c() = $a() + $b();
} else {
$SETBAD(c());
}',
You can reduce code repetition using the C "PDL_BAD_CODE" macro, supplying only the "Code" section:
Code => '
#ifdef PDL_BAD_CODE
if ( $ISGOOD(a()) && $ISGOOD(b()) ) {
#endif PDL_BAD_CODE
$c() = $a() + $b();
#ifdef PDL_BAD_CODE
} else {
$SETBAD(c());
}
#endif PDL_BAD_CODE
',
As of 2.073, you can also use "PDL_IF_BAD(iftrue,iffalse)":
Code => '
PDL_IF_BAD(if ( $ISGOOD(a()) && $ISGOOD(b()) ) {,)
$c() = $a() + $b();
PDL_IF_BAD(} else $SETBAD(c());,)
',
π Interfacing your own/library functions using PP π
Now, consider the following: you have your own C function (that may in fact be part of some library you want to interface to PDL) which takes as arguments two pointers to vectors of double:
void myfunc(int n,double *v1,double *v2);
The correct way of defining the PDL function is
pp_def('myfunc',
Pars => 'a(n); [o]b(n);',
GenericTypes => ['D'],
Code => 'myfunc($SIZE(n),$P(a),$P(b));'
);
The "$P("par")" syntax returns a pointer to the first element and the other elements are guaranteed to lie after that.
Notice that here it is possible to make many mistakes. First, $SIZE(n) must be used instead of "n". Second, you shouldn't put any loops in this code. Third, here we encounter a new hash key recognised by PDL::PP : the "GenericTypes" declaration tells PDL::PP to ONLY GENERATE THE TYPELOOP FOP THE LIST OF TYPES SPECIFIED. In this case "double". This has two advantages. Firstly the size of the compiled code is reduced vastly, secondly if non-double arguments are passed to myfunc() PDL will automatically convert them to double before passing to the external C routine and convert them back afterwards.
One can also use "Pars" to qualify the types of individual arguments. Thus one could also write this as:
pp_def('myfunc',
Pars => 'double a(n); double [o]b(n);',
Code => 'myfunc($SIZE(n),$P(a),$P(b));'
);
The type specification in "Pars" exempts the argument from variation in the typeloop - rather it is automatically converted to and from the type specified. This is obviously useful in a more general example, e.g.:
void myfunc(int n,float *v1,long *v2);
pp_def('myfunc',
Pars => 'float a(n); long [o]b(n);',
GenericTypes => ['F'],
Code => 'myfunc($SIZE(n),$P(a),$P(b));'
);
Note we still use "GenericTypes" to reduce the size of the type loop, obviously PP could in principle spot this and do it automatically though the code has yet to attain that level of sophistication!
Finally note when types are converted automatically one MUST use the "[o]" qualifier for output variables or you hard-won changes will get optimised away by PP!
okay bestie, when you're interfacing a big library, you can automate the interfacing even further. Perl can help you again(!) with this. Lots of libraries have certain calling conventions, and you can totally exploit that. In short, you can write a little parser (which is really not difficult in Perl) that then generates the calls to pp_def from parsed descriptions of the functions in that library. For an example, please check the Slatec interface in the Lib tree of the PDL distribution. If you want to check (during debugging) which calls to PP functions your Perl code generated, a little helper package comes in handy β it replaces the PP functions by identically named ones that dump their arguments to stdout.
Just say
perl -MPDL::PP::Dump myfile.pd
to see the calls to pp_def and friends. Try it with ops.pd and slatec.pd. If you're interested (or want to enhance it), the source is in Basic/Gen/PP/Dump.pm periodt.
β¨ Other macros in the Code section β¨
Macros: So far we have encountered the $SIZE, $GENERIC and $P macros. Now we are going to quickly explain the other macros that are expanded in the Code section of PDL::PP along with examples of their usage. it's giving secret menu.
$T
The $T macro is used for type switches. This is very useful when you have to use different external (e.g. library) functions depending on the input type of arguments. The general syntax is
$Ttypeletters(type_alternatives)
where "typeletters" is a permutation of a subset of the letters "BSULNQFDGC" which stand for Byte, Short, Ushort, etc. and "type_alternatives" are the expansions when the type of the PP operation is equal to that indicated by the respective letter. Let's illustrate this incomprehensible description by an example. Assuming you have two C functions with prototypes
void float_func(float *in, float *out);
void double_func(double *in, double *out);
which do basically the same thing but one accepts float and the other double pointers. You could interface them to PDL by defining a generic function "foofunc" (which will call the correct function depending on the type of the transformation):
pp_def('foofunc',
Pars => ' a(n); [o] b();',
Code => ' $TFD(float,double)_func ($P(a),$P(b));'
GenericTypes => [qw(F D)],
);
There is a limitation that the comma-separated values cannot have parentheses. so don't try it, fr fr.
$PP
The $PP macro is used for a so called physical pointer access. The physical refers to some internal optimisations of PDL (for those who are familiar with the PDL core we are talking about the vaffine optimisations). This macro is mainly for internal use and you shouldn't need to use it in any of your normal code. lowkey leave this one to the internals.
$PPSYM
The $PPSYM() macro is replaced by the value of "ppsym" in PDL::Types for the loop type, or that of the given parameter, similar to $GENERIC(). This is useful for e.g. macros that vary by that, avoiding the need for things like $TXY(X,Y). Another benefit is that if an operation's GenericTypes get extended, this macro will still be correct. slay, future-proof behavior.
$COMP (and the OtherPars section)
The $COMP macro is used to access non-pdl values in the code section. Its name is derived from the implementation of transformations in PDL. The variables you can refer to using $COMP are members of the ``compiled'' structure that represents the PDL transformation in question but does not yet contain any information about dimensions (for further details check PDL::Internals). However, you can treat $COMP just as a black box without knowing anything about the implementation of transformations in PDL. So when would you use this macro? Its main usage is to access values of arguments that are declared in the "OtherPars" section of a pp_def definition. But then you haven't heard about the "OtherPars" key yet?! Let's have another example that illustrates typical usage of both new features:
pp_def('pnmout',
Pars => 'a(m)',
OtherPars => "PerlIO *fp",
GenericTypes => [qw(B U S L)],
Code => '
if (PerlIO_write($COMP(fp),$P(a),len) != len)
$CROAK("Error writing pnm file");
');
This function is used to write data from a pdl to a file. The file descriptor is passed as a string into this function. This parameter does not go into the "Pars" section since it cannot be usefully treated like a pdl but rather into the aptly named "OtherPars" section. Parameters in the "OtherPars" section follow those in the "Pars" section when invoking the function, i.e.
open FILE,">out.dat" or die "couldn't open out.dat";
pnmout($pdl,'FILE');
When you want to access this parameter inside the code section you have to tell PP by using the $COMP macro, i.e. you write $COMP(fp) as in the example. Otherwise PP wouldn't know that the "fp" you are referring to is the same as that specified in the "OtherPars" section.
Another use for the "OtherPars" section is to set a named dimension in the signature. Let's have an example how that is done:
pp_def('setdim',
Pars => '[o] a(n)',
OtherPars => 'int ns => n',
Code => 'loop(n) %{ $a() = n; %}',
);
This says that the named dimension "n" will be initialised from the value of the other parameter "ns" which is of integer type (I guess you have realised that we use the "CType From => named_dim" syntax). As of 2.082, this can be used to set the size of a dimension not used in any "Pars". Now you can call this function in the usual way:
setdim(($x=null),5);
print $x;
[ 0 1 2 3 4 ]
Admittedly this function is not very useful but it demonstrates how it works. If you call the function with an existing pdl and you don't need to explicitly specify the size of "n" since PDL::PP can figure it out from the dimensions of the non-null pdl. In that case you just give the dimension parameter as -1:
$x = hist($y);
setdim($x,-1);
The default values available via $COMP() are the "OtherPars" as noted above, which get copied in. However, this can be added to (previous to 2.058, replaced) by supplying "Comp" and/or "MakeComp" keys (the defaults will happen first):
pp_def(
'diagonal',
OtherPars => 'SV *list',
Comp => 'PDL_Indx whichdims_count; PDL_Indx whichdims[$COMP(whichdims_count)];',
MakeComp => '
PDL_Indx i;
PDL_Indx *tmp= PDL->packdims(list,&($COMP(whichdims_count)));
if (!tmp) $CROAK("Failed to packdims for creating");
if ($COMP(whichdims_count) < 1)
$CROAK("Diagonal: must have at least 1 dimension");
$DOCOMPALLOC(); /* malloc()s the whichdims */
for(i=0; i<$COMP(whichdims_count); i++)
$COMP(whichdims)[i] = tmp[i];
free(tmp);
/* ... */
',
# ...
);
The "MakeComp" code is placed in the pdl_run_(funcname), so access to "Pars" (which will just be "pdl "s)/"OtherPars" values is just via their names, not a macro. The default code (which also applies to "OtherPars") makes a copy of values where it knows how to do so, including "SV" and "char*".
You can also provide a "CompFreeCodeComp" key, in case your "MakeComp" needs tidying up after it.
As of 2.058, you can instead give a C99 "incomplete array" type parameter as an "OtherPars" entry:
pp_def(
'diagonal',
OtherPars => 'PDL_Indx whichdims[]',
MakeComp => '
if ($COMP(whichdims_count) < 1)
$CROAK("Diagonal: must have at least 1 dimension");
/* ... */
',
# ...
);
There is an XS typemap entry (for "PDL_Indx" and "pdl*" array types for now) that relies on a "(varname)_count" variable being declared in the XS "INPUT" section (PP does this for you), to extract the index numbers from an array-ref parameter, and sets the count variable to the right value. PP then makes a copy of the data available. The C function (here, "pdl_run_diagonal")'s caller (here, the generated XS function) is responsible for freeing the array passed in (here, PDL's "smalloc" function is used, so the user need do nothing different).
XS-only OtherPars
As of 2.083, you can prefix the names of "OtherPars" with "$", e.g.
pp_def('minus',
OtherPars => 'int $swap',
# ...
);
This will mean they are available in "HdrCode" and "FtrCode", but not elsewhere in the generated code (e.g. "MakeComp", "Code"). sneaky, love that.
β¨ Other functions in the Code section β¨
The only PP function that we have used in the examples so far is "loop". Additionally, there are currently two other functions which are recognised in the "Code" section:
broadcastloop
As we heard above the signature of a PP defined function defines the dimensions of all the pdl arguments involved in a primitive operation. However, you often call the functions that you defined with PP with pdls that have more dimensions than those specified in the signature. In this case the primitive operation is performed on all subslices of appropriate dimensionality in what is called a broadcast loop (see also overview above and PDL::Indexing). Assuming you have some notion of this concept you will probably appreciate that the operation specified in the code section should be optimised since this is the tightest loop inside a broadcast loop. However, if you revisit the example where we define the "pnmout" function, you will quickly realise that looking up the "IO" file descriptor in the inner broadcast loop is not very efficient when writing a pdl with many rows. A better approach would be to look up the "IO" descriptor once outside the broadcast loop and use its value then inside the tightest broadcast loop. This is exactly where the "broadcastloop" function comes in handy. Here is an improved definition of "pnmout" which uses this function:
pp_def('pnmout',
Pars => 'a(m)',
OtherPars => "PerlIO *fp",
GenericTypes => [qw(B U S L)],
Code => '
int len;
len = $SIZE(m) * sizeof($GENERIC());
broadcastloop %{
if (PerlIO_write($COMP(fp),$P(a),len) != len)
$CROAK("Error writing pnm file");
%}
');
Okay so here's the tea π« β normally, all the C code you drop into the "Code" section gets auto-plopped inside a broadcast loop, because PP is a caring bestie and generates the wrapping C for you. BUT π
if you explicitly use the "broadcastloop" function, PDL::PP clock that immediately and is like "oh periodt, she knows what she's doing" and does not wrap your code in an extra broadcast loop.
The vibe of that? Everything you write outside the broadcast loop runs exactly once per transformation β¨, and only the code inside the surrounding "%{ ... %}" pair gets dropped into the tightest broadcast loop. This is chef's kiss π when you want a decision (or literally any other code, especially CPU-intensive code) to run only once per thread, i.e.
pp_addhdr('
#define RAW 0
#define ASCII 1
');
pp_def('do_raworascii',
Pars => 'a(); b(); [o]c()',
OtherPars => 'int mode',
Code => ' switch ($COMP(mode)) {
case RAW:
broadcastloop %{
/* do raw stuff */
%}
break;
case ASCII:
broadcastloop %{
/* do ASCII stuff */
%}
break;
default:
$CROAK("unknown mode");
}'
);
β¨ types β¨
The types function is giving the same energy as the $T macro, except β and this is the slay part π β with "types" the code in the block that follows (delimited by "%{" and "%}", as always, you know the drill) runs for all those cases where the operation's datatype is any of the types represented by the letters in the argument to "type", e.g.
Code => '...
types(BSUL) %{
/* do integer type operation */
%}
types(FD) %{
/* do floating point operation */
%}
...'
Please use this idiom (it's straight out of PDL::Math) so you don't have to suffer π when new types show up and your code needs to just work with them:
use PDL::Types qw(types);
my @Rtypes = grep $_->real, types();
my @Ctypes = grep !$_->real, types();
# ...
my $got_complex = PDL::Core::Dev::got_complex_version($name, 2);
my $complex_bit = join "\n",
map 'types('.$_->ppsym.') %{$'.$c.'() = c'.$name.$_->floatsuffix.'($'.$x.'(),$'.$y.'());%}',
@Ctypes;
my $real_bit = join "\n",
map 'types('.$_->ppsym.') %{$'.$c.'() = '.$name.'($'.$x.'(),$'.$y.'());%}',
@Rtypes;
($got_complex ? $complex_bit : '') . $real_bit;
(although bestie, first check whether tgmath.h already has a type-generic version of the function you want to call β in which case everything above becomes completely unnecessary π).
πΈ The RedoDimsCode Section πΈ
The "RedoDimsCode" key is optional β¨ and it's the one you reach for when you need to compute the dimensions of ndarrays at runtime, specifically in those cases where the standard rules for computing dimensions from the signature are just not giving. The contents of the "RedoDimsCode" entry get interpreted exactly the same way the Code section does β i.e. PP macros get expanded and the result is read as C code. What the code is for: setting the size of some dimensions that show up in the signature π. Storage allocation, broadcastloops and all that good stuff get set up as if the computed dimension had been in the signature the whole time. So in your code you first figure out the desired size of a named dimension in the signature, then assign that value to it via the $SIZE() macro. Periodt π
Consider this situation, fr fr. You're interfacing an external library routine that needs a temporary array for workspace passed as an argument. Two input data arrays get passed in: p(m) and x(n). The output data array is y(n). The routine needs a workspace array of length n+m*m, and you want the storage created automatically, just like it would be for any ndarray flagged with [t] or [o]. So you're like "can I just sayβ"
pp_def( "myexternalfunc",
Pars => " p(m); x(n); [o] y; [t] work(n+m*m); ", ...
...but nope π that does not work, because PP can't interpret expressions with arithmetic in the signature. Instead you write:
pp_def(
"myexternalfunc",
Pars => ' p(m); x(n); [o] y(); [t] work(wn); ',
RedoDimsCode => '$SIZE(wn) = $SIZE(n) + $SIZE(m) * $SIZE(m);',
Code => '
externalfunc( $P(p), $P(x), $SIZE(m), $SIZE(n), $P(work) );
'
);
As of 2.075, you can use the dimensions of passed-in ndarrays as they are available when the "RedoDimsCode" is run π. Before the code in the Code section executes, PP will create the proper storage for "work" (one area per POSIX thread, in case of broadcasting that multi-threads β the user cannot supply this, so don't even try π
). Note that you only took the first dimension of "p" and "x" because the user may have sent ndarrays with extra broadcasting dimensions. Iconic, honestly.
You can also use "RedoDimsCode" to set the dimension of an ndarray flagged with [o]. In that case you set the dimensions for the named dimension in the signature using $SIZE() just like in the example above. BUT β because the ndarray is flagged with [o] instead of [t], broadcasting dimensions will be added if required, just as if the size of the dimension had been computed from the signature according to the usual rules. Here's one from PDL::Math:
pp_def("polyroots",
Pars => 'cr(n); ci(n); [o]rr(m); [o]ri(m);',
RedoDimsCode => '$SIZE(m) = $SIZE(n)-1;',
The input ndarrays are the real and imaginary parts of complex coefficients of a polynomial π. The output ndarrays are the real and imaginary parts of the roots. There are "n" roots to an "n"th order polynomial, and such a polynomial has "n+1" coefficients (the zero-th through the "n"th). In this example, broadcasting works correctly π. That is, the first dimension of the output ndarray gets its dimension adjusted, but other broadcasting dimensions get assigned just as if there were no "RedoDimsCode" at all. Chef's kiss.
π¦ RedoDims passed directly π¦
A "RedoDimsCode" value like the above gets processed β macros expanded, type-generic loops added, the whole glow-up. But for very specific purposes you may not want that processing done to your dimension-updating code. Probably in "slice"-like functions, iykyk.
So instead of passing a "RedoDimsCode" value, you can pass a "RedoDims" value (which is what the "RedoDimsCode" would otherwise get processed into anyway). And because you'll probably want to access the ndarrays, PP provides the following macros for you π«Ά. Their names assume you'll have the first parameter as "PARENT" and the second as "CHILD" β which is exactly the case if you passed a true "P2Child" value, which you will basically always want to do for this scenario.
β RedoDims generated from EquivPDimExpr and EquivDimCheck β
Another way to generate the "RedoDims" code is to supply a "EquivPDimExpr" and maybe a "EquivDimCheck":
pp_def(
'xchg',
OtherPars => 'PDL_Indx n1; PDL_Indx n2;',
TwoWay => 1,
P2Child => 1,
AffinePriv => 1,
EquivDimCheck => '
if ($COMP(n1) <0) $COMP(n1) += $PARENT(broadcastids[0]);
if ($COMP(n2) <0) $COMP(n2) += $PARENT(broadcastids[0]);
if (PDLMIN($COMP(n1),$COMP(n2)) <0 ||
PDLMAX($COMP(n1),$COMP(n2)) >= $PARENT(broadcastids[0]))
$CROAK("One of dims %d, %d out of range: should be 0<=dim<%d",
$COMP(n1),$COMP(n2),$PARENT(broadcastids[0]));',
EquivPDimExpr => '
(($CDIM == $COMP(n1)) ? $COMP(n2) :
($CDIM == $COMP(n2)) ? $COMP(n1) :
$CDIM)
',
);
"EquivPDimExpr" is evaluated inside a loop π, and the value of the relevant dimension is available using the macro $CDIM, exactly as shown up there π.
π Typemap handling in the OtherPars section π
The "OtherPars" section we talked about earlier is very often absolutely crucial when you interface external libraries with PDL. But in many cases those external libraries either use derived types or pointers of various types, and that's where it gets spicy πΆοΈ.
The standard Perl way to handle this is a typemap file. This gets discussed in some detail in perlxs in the standard Perl documentation π. In PP the functionality is very similar β so you can create a typemap file in the directory where your PP file lives, and when it's built, it gets read in automatically to figure out the appropriate translation between the C type and Perl's built-in type. No extra steps, no stress π .
For instance, the "gsl_spline_init" function has the following C declaration:
int gsl_spline_init(gsl_spline * spline,
const double xa[], const double ya[], size_t size);
Clearly the "xa" and "ya" arrays are candidates for being passed in as ndarrays, and the "size" argument is just the length of these ndarrays, so that one can be handled by the $SIZE() macro in PP. Write an "OtherPars" declaration of the form:
OtherPars => 'gsl_spline *spl'
and write a short typemap file which handles this type:
TYPEMAP
gsl_spline * T_PTR
and use it in the code:
pp_def('init_meat',
Pars => 'double x(n); double y(n);',
OtherPars => 'gsl_spline *spl',
Code =>'gsl_spline_init,($COMP(spl),$P(x),$P(y),$SIZE(n)));'
);
where I have removed a macro wrapper call, but that would obscure the discussion π.
You can also have "OtherPars" entries that are "incomplete arrays" of "pdl*", both for input and output:
OtherPars => 'pdl *ins[]', # $COMP(ins_count) will be available
# OR
OtherPars => '[o] pdl *outs[]', # update $COMP(outs_count) in your code
β οΈ Heads up bestie: the output typemap entry does a"free"on the array of"pdl*"pointers, so make sure you"malloc"it in your code, and do it without leaking. No cap, this one matters.
π OtherPars as outputs π
As of 2.081, you can specify an "OtherPar" as an output β¨. It looks like this:
pp_def('output_op',
Pars => 'in(n=2)',
OtherPars => '[o] PDL_Anyval v0; [o] PDL_Anyval v1',
Code => '
pdl_datatypes dt = $PDL(in)->datatype;
ANYVAL_FROM_CTYPE($COMP(v0), dt, $in(n=>0));
ANYVAL_FROM_CTYPE($COMP(v1), dt, $in(n=>1));
',
);
The passed-in stack SV gets mutated in place, so this code will then work:
output_op([5,7], my $v0, my $v1);
is_deeply [$v0,$v1], [5,7], 'output OtherPars work';
($v0, $v1) = output_op([5,7]); # you can omit them, then they get returned
is_deeply [$v0,$v1], [5,7], 'output OtherPars work 1a';
π¨ Okay important one: an operation with output"OtherPars"cannot broadcast, because that would cause undefined results π. A runtime check gets generated that throws an exception if any"Par"would cause broadcasting. So don't try it, the check will catch you fr fr.
okay so about that OtherPars syntax, bestie β the [o] goes before the type, while it goes after the type in Pars. Why?? because they literally felt this was the best way to dodge ambiguity given that C types can have [] in them. π
iykyk.
This relies on the relevant OtherPar having an OUTPUT entry in an XS typemap. π«Ά
As of 2.083, it's also possible to specify OtherPars as [io], which means they must be supplied (rather than being optional, like an [o] one), but they'll still be updated after the operation has finished. β¨
Other useful PP keys in data operation definitions
You have already heard about the OtherPars key. Currently, there are not many other keys for a data operation that will be useful in normal (whatever that is) PP programming. In fact, it would be interesting to hear about a case where you think you need more than what is provided at the moment. Please speak up on one of the PDL mailing lists! πΈ Most other keys recognised by pp_def are only really useful for what we call slice operations (see also above).
One thing that is strongly being planned is variable number of arguments, which will be a little tricky. π
An incomplete list of the available keys:
Inplace
Setting this key marks the routine as working inplace β ie the input and output ndarrays are the same. An example is $x->inplace->sqrt() (or sqrt(inplace($x))).
Inplace => 1β use when the routine is a unary function, such assqrt.Inplace => ['a']β if there are more than one input ndarrays, specify the name of the one that can be changed inplace using an array reference.Inplace => ['a','b']β if there are more than one output ndarray, specify the name of the input ndarray and output ndarray in a 2-element array reference. This probably isn't needed, but left in for completeness.
If bad values are being used, care must be taken to ensure the propagation of the badflag when inplace is being used; consider this excerpt from Basic/Bad/bad.pd:
pp_def('setbadtoval',HandleBad => 1,
Pars => 'a(); [o]b();',
OtherPars => 'double newval',
Inplace => 1,
CopyBadStatusCode => 'PDL->propagate_badflag( b, 0 );',
...
Since this routine removes all bad values, the output ndarray had its bad flag cleared. This is then propagated to both parents and children. π
NOTE: one idea is that the documentation for the routine could be automatically flagged to indicate that it can be executed inplace, ie something similar to howHandleBadsetsBadDocif it's not supplied (it's not an ideal solution).
FTypes
# in slices.pd
FTypes => {CHILD => '$COMP(totype)'},
The value is a hash-ref mapping parameter-names to an expression giving an override of the type for that parameter. The example above shows the type being overridden to the OtherPars totype. π
OtherParsDefaults
OtherPars => 'int a; int b',
OtherParsDefaults => { b => 0 },
Allows specifying default values for OtherPars. It is an error to specify a default for one that is before another that does not have a default. π¨
ArgOrder
Pars => 'x(); y(); [o]z()'
OtherPars => 'int a; int b',
ArgOrder => [qw(x y a b z)],
# or, a non-reference true value to enable flexible arg-handling and
# move defaultable to the end, followed by output ndarrays then OtherPars
Pars => 'x(); y(); [o]z()'
OtherPars => 'int a; int b',
ArgOrder => 1,
Allows specifying a different order for providing the operation's arguments. This affects only the generated XS (not C pdl_run_(name)) parameter list; the internal ordering of pdl* in various C arrays is unaffected. β¨
Providing a non-reference true value enables flexible argument-handling and moves defaultable to the end, followed by output ndarrays then output OtherPars. Also, all outputs (ndarray and OtherPars) will be returned on the stack, even if supplied as arguments. π
It is an error to specify arguments that are not provided, or to give a false value, or to have optional arguments after mandatory ones.
XS argument-handling change
This also changes PP's XS argument handling; normally you can specify:
- just the input/io arguments
- (if the operation has default values provided) those plus values for all arguments with defaults
- all of those plus output arguments, in other words all non-
[t]arguments
With ArgOrder given, optional arguments (outputs and ones with defaults) will be filled in from the leftmost missing one. π¦
HdrCode
This is C code that is inserted in the XS function before the call to the generated pdl_run_(funcname). It will have access to all the Pars and OtherPars as C values.
FtrCode
As of 2.083. This is C code that is inserted in the XS function after the call to the generated pdl_run_(funcname). It will have access to all the Pars and OtherPars as C values. π
Other PDL::PP functions to support concise package definition
So far, we have described the pp_def and pp_done functions. PDL::PP exports a few other functions to aid you in writing concise PDL extension package definitions. π§
pp_addhdr
Often when you interface library functions as in the above example you have to include additional C include files. Since the XS file is generated by PP we need some means to make PP insert the appropriate include directives in the right place into the generated XS file. To this end there is the pp_addhdr function. This is also the function to use when you want to define some C functions for internal use by some of the XS functions (which are mostly functions defined by pp_def). By including these functions here you make sure that PDL::PP inserts your code before the point where the actual XS module section begins and will therefore be left untouched by xsubpp (cf. perlxs and perlxstut man pages).
A typical call would be
pp_addhdr('
#include <unistd.h> /* we need defs of XXXX */
#include "libprotos.h" /* prototypes of library functions */
#include "mylocaldecs.h" /* Local decs */
static void do_the real_work(PDL_Byte * in, PDL_Byte * out, int n)
{
/* do some calculations with the data */
}
');
This ensures that all the constants and prototypes you need will be properly included and that you can use the internal functions defined here in the pp_defs, e.g.: β
pp_def('barfoo',
Pars => ' a(n); [o] b(n)',
GenericTypes => ['B'],
Code => ' PDL_Indx ns = $SIZE(n);
do_the_real_work($P(a),$P(b),ns);
',
);
pp_addpm
In many cases the actual PP code (meaning the arguments to pp_def calls) is only part of the package you are currently implementing. Often there is additional Perl code and XS code you would normally have written into the pm and XS files which are now automatically generated by PP. So how to get this stuff into those dynamically generated files? Fortunately, there are a couple of functions, generally called pp_addXXX that assist you in doing this. π«Ά
Let's assume you have additional Perl code that should go into the generated pm-file. This is easily achieved with the pp_addpm command:
pp_addpm(<<'EOD');
=head1 NAME
PDL::Lib::Mylib -- a PDL interface to the Mylib library
=head1 DESCRIPTION
This package implements an interface to the Mylib package with full
broadcasting and indexing support (see L<PDL::Indexing>).
=cut
use PGPLOT;
=head2 use_myfunc
this function applies the myfunc operation to all the
elements of the input pdl regardless of dimensions
and returns the sum of the result
=cut
sub use_myfunc {
my $pdl = shift;
myfunc($pdl->clump(-1),($res=null));
return $res->sum;
}
EOD
pp_add_exported
You have probably got the idea. In some cases you also want to export your additional functions. To avoid getting into trouble with PP which also messes around with the @EXPORT array you just tell PP to add your functions to the list of exported functions:
pp_add_exported('use_myfunc gethynx');
pp_add_isa
The pp_add_isa command works like the the pp_add_exported function. The arguments to pp_add_isa are added the @ISA list, e.g.
pp_add_isa(' Some::Other::Class ');
pp_bless
If your pp_def routines are to be used as object methods use pp_bless to specify the package (i.e. class) to which your pp_defed methods will be added. For example, pp_bless('PDL::MyClass'). The default is PDL if this is omitted.
The value given here (or the default, PDL), anywhere in the .pd file, will be the package into which all PP operations get added, even for operations whose pp_def was called before the pp_bless. This is because that package is inserted at the start of the generated XS code by pp_done. The only way this changes is if pp_addxs is called, which will add the given code (or none if an empty string is given) to the $::PDLPACK package, and then changes the package to the pp_bless value. For historical reasons, this cannot be changed. So, to have several different packages in one .pd file, do something like this:
# any pp_def up till now will get put in PDL::Pack2
pp_bless('PDL::Pack1');
pp_addxs('');
pp_def('func1', ...);
pp_bless('PDL::Pack2');
pp_addxs('');
pp_def('otherfunc', ...);
pp_addxs
Sometimes you want to add extra XS code of your own (that is generally not involved with any broadcasting/indexing issues but supplies some other functionality you want to access from the Perl side) to the generated XS file, for example π
pp_addxs('','
# Determine endianness of machine
int
isbigendian()
CODE:
unsigned short i;
PDL_Byte *b;
i = 42; b = (PDL_Byte*) (void*) &i;
if (*b == 42)
RETVAL = 0;
else if (*(b+1) == 42)
RETVAL = 1;
else
croak("Impossible - machine is neither big nor little endian!!\n");
OUTPUT:
RETVAL
');
Especially pp_add_exported and pp_addxs should be used with care. PP uses PDL::Exporter, hence letting PP export your function means that they get added to the standard list of function exported by default (the list defined by the export tag ``:Func''). If you use pp_addxs you shouldn't try to do anything that involves broadcasting or indexing directly. PP is much better at generating the appropriate code from your definitions. π
pp_add_boot
Okay bestie so, you might wanna drop some code into the BOOT section of the XS file β and if you're sitting there like "girl what is a BOOT section", go read perlxs, it's giving homework but it's necessary. Anyway, it's easy peasy with the pp_add_boot command:
pp_add_boot(<<EOB);
descrip = mylib_initialize(KEEP_OPEN);
if (descrip == NULL)
croak("Can't initialize library");
GlobalStruc->descrip = descrip;
GlobalStruc->maxfiles = 200;
EOB
β¨ pp_export_nothing β¨
So by default, PP.pm is out here putting every single sub you defined with pp_def into the output .pm file's EXPORT list. Which is fine, until it's not β like when you're making a subclassed object and you genuinely do not want any methods exported. (iykyk β think situations where the methods only ever get called with the $object->method syntax.)
For those cases you can call pp_export_nothing() to wipe the export list clean. Example, and this goes at the end of the .pd file:
pp_export_nothing();
pp_done();
β¨ pp_core_importList β¨
By default, PP.pm drops the 'use Core;' line into the output .pm file. Which imports Core's exported names straight into the current namespace β and that's a whole problem if you're over-riding one of Core's methods in the current file. You end up with messages like "Warning: sub sumover redefined in file subclass.pm" when you run the program. Not the vibe. π
For these cases, pp_core_importList lets you change what gets imported from Core.pm. For example:
pp_core_importList('()')
That would result in
use Core();
being generated in the output .pm file β which means no names at all get imported from Core.pm. Similarly, calling
pp_core_importList(' qw/ barf /')
would result in
use Core qw/ barf/;
being generated in the output .pm file, meaning just 'barf' gets imported from Core.pm. Slay, selective. π
β¨ pp_setversion β¨
This one sets the .pm and .xs files' versions at the same time β so you avoid any unnecessary version-skew between the two. To use it, just do this in your .pd file, probably near the top:
our $VERSION = '0.0.3';
pp_setversion($VERSION);
# Then, in your Makefile.PL:
my @package = qw(FFTW3.pd FFTW3 PDL::FFTW3);
my %descriptor = pdlpp_stdargs(\@package);
$descriptor{VERSION_FROM} = 'FFTW3.pd'; # EUMM can parse the format above
But do not use this if you use Module::Build::PDL. Go read that module's documentation for the details, bestie, I'm not playing. π
β¨ pp_deprecate_module β¨
If a particular module has been deemed obsolete, this function marks it as deprecated. Effect? It emits a warning whenever a user tries to "use" the module. And the generated POD for the module also carries a deprecation notice. You can pass the replacement module as an argument, like this:
pp_deprecate_module( infavor => "PDL::NewNonDeprecatedModule" );
Heads up though β this function affects only the runtime warning and the POD. That's it. That's the whole reach.
π MAKING YOUR PP FUNCTION PRIVATE π
Okay so picture this: you've got a function in your module called PDL::foo that uses the PP function "bar_pp" to do all the heavy lifting. But you do not want to advertise that "bar_pp" exists. Secret menu energy. π€«
To do this, you must move your PP function to the top of your module file, then call
pp_export_nothing()
to clear the "EXPORT" list. Then, to make sure no documentation gets generated (not even the default PP docs), set
Doc => undef
and to stop the function from being added to the symbol table, set
PMFunc => ''
in your pp_def declaration β peep Image2D.pd for a working example.
That combo effectively makes your PP function "private." Now, real talk: it's always still accessible via PDL::bar_pp, because that's just Perl's module design doing its thing. But making it private means the user has to go very far out of their way to use it β so they shoulder the consequences! Their choice, their problem. π
π¦ SLICE OPERATION π¦
Okay, the slice operations need a much more intimate knowledge of PDL internals than the data operations do. And on top of that, the complexity of the issues involved is considerably higher than your average data operation. Not gonna lie to you. π
But β and this is the iconic part β functions generated using the slice operations are literally at the heart of the index manipulation and dataflow capabilities of PDL. So it's worth it. You can get started by reading the section on "P2Child".
Also, there are a lot of dirty issues with virtual ndarrays and vaffines which we shall entirely skip here. Skipping. Not touching. Moving on. π
πΈ Slices and bad values πΈ
Slice operations need to be able to handle bad values. The easiest thing to do is go look at Basic/Slices/slices.pd to see how this works. Just read it, bestie, it'll click.
Along with "BadCode", there are also the "BadBackCode" and "BadRedoDimsCode" keys for "pp_def". However β any "EquivCPOffsCode" should not need changing, since any changes are absorbed into the definition of the $EQUIVCPOFFS() macro (i.e. it is handled automatically by PDL::PP). So don't go messing with what's already handled. Periodt.
π Handling of "warn" and "barf" in PP Code π
For printing warning messages or aborting/dieing, you can call "warn" or "barf" from PP code. However, you should be aware that these calls have been redefined using C preprocessor macros to "PDL->barf" and "PDL->warn".
Why? Because these redefinitions are in place to keep you from inadvertently calling perl's "warn" or "barf" directly β which can cause segfaults during pthreading (i.e. processor multi-threading). So like, don't fight the macros. π«Ά
PDL's own versions of "barf" and "warn" will queue-up warning or barf messages until after pthreading is completed, and then call the perl versions of these routines. Orderly. Respectful. Iconic.
See PDL::ParallelCPU for more info on pthreading.
NB As of2.064, it is highly recommended that you do not call"barf"at all in PP code, but instead use$CROAK(). This will return a"pdl_error"which will transparently be used to throw the correct exception in Perl code, but can be handled suitably by non-Perl callers. Do the new thing, bestie. π
β¨ MAKEFILES FOR PP FILES β¨
If you're gonna generate a package from your PP file (typical file extensions are ".pd" or ".pp" for the files containing PP code) β it is easiest and safest to leave generation of the appropriate commands to the Makefile. Let a machine do it. That's the whole point of machines.
In the following we will outline the typical format of a Perl Makefile to automatically build and install your package from a description in a PP file. Most of the rules to build the xs, pm and other required files from the PP file are already predefined in the PDL::Core::Dev package. We just have to tell MakeMaker to use it. Easy.
In most cases you can define your Makefile like
use PDL::Core::Dev; # Pick up development utilities
use ExtUtils::MakeMaker;
$package = ["mylib.pd",Mylib,PDL::Lib::Mylib,'',1];
%hash = pdlpp_stdargs($package);
$hash{OBJECT} .= ' additional_Ccode$(OBJ_EXT) ';
WriteMakefile(%hash);
sub MY::postamble { pdlpp_postamble($package); }
# additional_Ccode.c
#include "pdl.h"
void ppcp(PDL_Byte *dst, PDL_Byte *src, int len)
{
int i;
for (i=0;i<len;i++) *dst++=*src++;
}
Here's the breakdown of the list in $package, in order, because order matters bestie:
- PP source file name
- the prefix for the produced files
- the whole package name
- the package to add XS functions to (empty string to use the same as the PP functions)
- a boolean to dictate whether to have PDL generate a separate C file for each PP function (for faster compilation)
That last feature is opt-in β as you have to avoid duplicate symbols when linking the library (so separate out C functions into their own file). You can modify the hash in whatever way you like, but it would be reasonable to stay within some limits so that your package will continue to work with later versions of PDL. Future-proofing, we love to see it. π
To make life even easier, PDL::Core::Dev defines the function "pdlpp_stdargs" that returns a hash with default values that can be passed (either directly or after appropriate modification) to a call to WriteMakefile. Currently, "pdlpp_stdargs" returns a hash where the keys are filled in as follows:
(
'NAME' => $mod,
VERSION_FROM => $src,
'TYPEMAPS' => [&PDL_TYPEMAP()],
'OBJECT' => "$pref\$(OBJ_EXT)",
PM => {"$pref.pm" => "\$(INST_LIBDIR)/$pref.pm"},
MAN3PODS => {"$pref.pm" => "\$(INST_MAN3DIR)/$mod.\$(MAN3EXT)"},
'INC' => &PDL_INCLUDE(),
'LIBS' => [''],
'clean' => {'FILES' => "$pref.xs $pref.pm $pref\$(OBJ_EXT)"},
)
Here, $src is the name of the source file with PP code, $pref the prefix for the generated .pm and .xs files, and $mod the name of the extension module to generate. Got it? Got it. π
If your "VERSION_FROM" provides a version, PP will use that to set the "XS_VERSION". If you need to influence the value of that variable so that XSLoader etc don't reject the loaded dynamic library, you can use this workaround in a "pp_addpm" (the "BEGIN" is because the "bootstrap" happens at runtime, and your code appears after that call, but with a "BEGIN" it will take place beforehand):
our $VERSION; BEGIN { $VERSION = '2.019106' };
our $XS_VERSION; BEGIN { $XS_VERSION = $VERSION };
π§ INTERNALS π§
The internals of the current version consist of a large table which gives the rules according to which things are translated, plus the subs which implement these rules.
Later on, it would be good to make the table modifiable by the user so that different things may be tried. Aspirational. We stan a roadmap. β¨
[Meta comment: here will hopefully be more in the future; currently, your best bet will be to read the source code :-( or ask on the list (try the latter first) ]
β C PREPROCESSOR MACROS β
As well as the above-mentioned "PDL_BAD_CODE" and "PDL_IF_BAD(iftrue,iffalse)", there are also "PDL_IF_GENTYPE_REAL(iftrue,iffalse)", "PDL_IF_GENTYPE_UNSIGNED", and "PDL_IF_GENTYPE_INTEGER":
$b() = PDL_IF_GENTYPE_INTEGER(0,NAN);
Macros doing macro things. Go off. π₯
β¨ Appendix A: Some keys recognised by PDL::PP β¨
Okay bestie, this is the glossary of vibes β the keys you can hand to pp_def and friends. Unless otherwise specified, the arguments are strings. That's the fine print, periodt. π
Parsβ defines the signature of your function. This is THE one, the main character, the blueprint of your whole pp situation. π«ΆOtherParsβ arguments which are not pdls. Default: nothing. It's a semi-colon separated list of arguments, like this:"OtherPars=>'int k; double value; PerlIO *fp'". See$COMP(x)and also the same entry in Appendix B. πCodeβ the actual code that implements the functionality. Several PP macros and PP functions are recognised inside that string value, so it's not just plain C cosplaying, iykyk. π₯HandleBadβ if set to1, the routine is assumed to support bad values, and the code in theBadCodekey gets used if bad values are present. It also sets things up so the$ISBAD()etc macros can be used. πΈ If set to0, it causes the routine to print a warning if any of the input ndarrays have their bad flag set. (Yeah. A warning. It's giving concerned friend energy.)BadCodeβ give the code to be used if bad values may be present in the input ndarrays. Only used if"HandleBad => 1". If"HandleBad"is true and"BadCode"is not supplied, the"Code"section will be reused, on the assumption it will use"#ifdef PDL_BAD_CODE"to handle bad values. As of 2.073, you can, and are recommended to, use"PDL_IF_BAD(iftrue,iffalse)". πCopyBadStatusCodeβ as of 2.079, this is deprecated due to being largely unnecessary π« β instead, just use$PDLSTATESETBAD(pdlname)in your"Code"section and the badflag setting will be propagated to all its parents and children. The default code here sets the bad flag of the output ndarrays if$BADFLAGCACHE()is true after the code has been evaluated. Sometimes"CopyBadStatusCode"is set to an empty string, with the responsibility of setting the badflag of the output ndarray left to the"BadCode"section (e.g. the"xxxover"routines inBasic/Primitive/primitive.pd). No cap, that's literally how it works. πGenericTypesβ an array reference. The array may contain any subset of the one-character strings given below, which specify which types your operation will accept. The meaning of each type is:
B - signed byte (i.e. signed char)
S - signed short (two-byte integer)
U - unsigned short
L - signed long (four-byte integer, int on 32 bit systems)
N - signed integer for indexing ndarray elements (platform & Perl-dependent size)
Q - signed long long (eight byte integer)
F - float
D - double
G - complex float
C - complex double
This is very useful (and important!) when interfacing an external library. Default: [qw/B S U L N Q F D/] β
Inplaceβ mark a function as being able to work inplace. Obsessed with the efficiency honestly:
Inplace => 1 if Pars => 'a(); [o]b();'
Inplace => ['a'] if Pars => 'a(); b(); [o]c();'
Inplace => ['a','c'] if Pars => 'a(); b(); [o]c(); [o]d();'
If bad values are being used, care must be taken to ensure the propagation of the badflag when inplace is being used; for instance see the code for "setbadtoval" in Basic/Bad/bad.pd. π«Ά
Docβ used to specify a documentation string in Pod format. See PDL::Doc for information on PDL documentation conventions. π Note: in the special case where the PP 'Doc' string is one line this is implicitly used for the quick reference AND the documentation! If the Doc field is omitted PP will generate default documentation (after all it knows about the Signature). And if you really want the function NOT to be documented in any way at this point (e.g. for an internal routine, or because you are doing it elsewhere in the code) explicitly specify"Doc=>undef". πBadDocβ contains the text returned by the"badinfo"command (in"perldl") or the-bswitch to the"pdldoc"shell script. In many cases, you will not need to specify this, since the information can be automatically created by PDL::PP. However, as befits computer-generated text, it's rather stilted; it may be much better to do it yourself! π (fr, nobody wants robot prose)NoPthreadβ optional flag to indicate the PDL function should not use processor threads (i.e. pthreads or POSIX threads) to split up work across multiple CPU cores. This option is typically set to1if the underlying PDL function is not threadsafe. If this option isn't present, then the function is assumed to be threadsafe. This option only applies if PDL has been compiled with POSIX threads enabled. π§΅PMCodeβ the whole point is letting you write your own Perl wrapper:
pp_def('funcname',
Pars => 'a(); [o] b();',
PMCode => 'sub PDL::funcname {
return PDL::_funcname_int(@_) if @_ == 2; # output arg "b" supplied
PDL::_funcname_int(@_, my $out = PDL->null);
$out;
}',
# ...
);
PDL functions allow "[o]" ndarray arguments into which you want the output saved. This is handy because you can allocate an output ndarray once and reuse it many times; the alternative would be for PDL to create a new ndarray each time, which may waste compute cycles or, more likely, RAM. πΈ
PDL functions check the number of arguments they are given, and call "croak" if given the wrong number. By default (with no "PMCode" supplied), any output arguments may be omitted, and PDL::PP provides code that can handle this by creating "null" objects, passing them to your code, then returning them on the stack.
If you do supply "PMCode", the rest of PDL::PP assumes it will be a string that defines a Perl function with the function's name in the "pp_bless" package ("PDL" by default). As the example implies, the PP-generated function name will change from "<funcname>", to "_<funcname>_int". As also shown above, you will need to supply all ndarrays in the exact order specified in the signature: output ndarrays are not optional, and the PP-generated function will not return anything. π¨ (read that twice, bestie)
PMFuncβ whenpp_defgenerates functions, it typically defines them in the PDL package. Then, in the.pmfile that it generates for your module, it typically adds a line that essentially copies that function into your current package's symbol table with code that looks like this:
*func_name = \&PDL::func_name;
It's a little bit smarter than that (it knows when to wrap that sort of thing in a BEGIN block, for example, and if you specified something different for pp_bless), but that's the gist of it. If you don't care to import the function into your current package's symbol table, you can specify
PMFunc => '',
PMFunc has no other side-effects, so you could use it to insert arbitrary Perl code into your module if you like. However, you should use pp_addpm if you want to add Perl code to your module. β¨
ReadDataFuncNameβ allows overriding the default function-name, for reading data transformed by this operation. Mainly used internally to set it to"NULL", in which case a default affine-orientated function will be called instead. πWriteBackDataFuncNameβ as above, but for writing transformed data from a child of this transformation back to the parent when"BackCode"is supplied. π¦AffinePrivβ flag to indicate this is an affine transformation whose"Priv"(contents of the"pdl_trans") contains data that will need allocating and freeing. π§GlobalNewβ if supplied, will prevent generation of an XS function, and assigns the generated C"run"function into the named slot in the"Core"struct. This is not used as of 2.058, and instead the relevant C functions are inpdlaffine.c. πP2Childβ forces"Pars"to be"PARENT"and"CHILD", the function's"GenericTypes"to be all of them, no"HaveBroadcasting"or"CallCopy", and turns on"DefaultFlow"(so do not supply any of those args). Intended for affine transformations with dataflow. πDefaultFlowβ if set, sets in the"pdl_transvtable"(see PDL::Internals) the"iflags"such that the trans will start with dataflow both forwards and backwards. Note that setting this to any value (including0) will trigger the behaviour. β οΈ (yes, even zero. sneaky.)HaveBroadcastingβ default true. If so, generate code implementing broadcasting (see PDL::Indexing). πCallCopyβ for parameters that get created, normally the"PDL->initialize"will be used (or on a subclass). If this is true (which is the default for simple functions i.e. 2-arg with 0-dim signatures), instead the first argument's"copy"method will be used. πTwoWayβ if true, sets in the"pdl_transvtable"(see PDL::Internals) the"iflags"such as to inform the trans's error checks connected to dataflow. πIdentityβ if true, sets"RedoDims""EquivCPOffsCode""HandleBad""P2Child""TwoWay"such that the function is a dataflowing identity transformation. πBackCodeβ for dataflowing functions, this value (which gets parsed) overrides the operation of that from children ndarrays to parents. πBadBackCodeβ same but taking account of bad values. π«EquivCPOffsCodeβ if supplied, allows concise control of copying to Child from Parent the data considered Equivalent at each given Offset (hence the name); the"Code"and"BackCode"will be generated from this. Very demure, very mindful. π§
Example:
pp_def(
'_clump_int',
OtherPars => 'int n',
P2Child => 1,
RedoDims => # omitted
EquivCPOffsCode => '
PDL_Indx i;
for(i=0; i<$PDL(CHILD)->nvals; i++) $EQUIVCPOFFS(i,i);
',
);
π¦ Appendix B: PP macros and functions π¦
β¨ Macros β¨
okay bestie, these are the little magic words PP swaps out for you at build time. every single one, no skips π
$variablename_from_sig()β this is how you access a pdl (by its name, like literally the name from the signature) that you specified in the signature. yes queen, just call it by name π$COMP(x)β reaches into the private data structure of this transformation and grabs a value. mainly used when you wanna use an argument that's specified in the"OtherPars"section π$SIZE(n)β gets replaced at runtime by the actual size of a named dimension (the one you specified in the signature, iykyk) β¨$GENERIC()β swapped out for the C type that equals the runtime type of the operation. it's giving "type, but make it dynamic" π$P(a)β a pointer to the data of the PDL namedain the signature. super useful for interfacing to C functions π₯$PP(a)β a physical pointer access to pdla. mainly for internal use, so like, don't @ it unless you know what you're doing π$TXYZ(AlternativeX,AlternativeY,AlternativeZ)β expansion alternatives according to the runtime type of the operation, where XXX is some string that gets matched by/[BSULNQFD+]/π$PDL(a)β returns a pointer to the pdl data structure (pdl *) of ndarrayaπ«Ά$ISBAD(a())β returns true if the value stored ina()equals the bad value for this ndarray. requires"HandleBad"being set to 1. no HandleBad, no service π$ISGOOD(a())β returns true if the value stored ina()does not equal the bad value for this ndarray. also requires"HandleBad"being set to 1 πΈ$SETBAD(a())β setsa()to equal the bad value for this ndarray. yep,"HandleBad"= 1 again, that's the vibe check π$PRIV()β lets you access fields in the"pdl_trans", like eg$PRIV(offs)π$CROAK()β returns a"pdl_error"with the (var-args) message you supply, and it adds the function name at the start, which will cause a"barf"within the Perl code. this is (as of 2.064) a change in PDL functions' API, so that callers can handle exceptions in their preferred way β which may not use Perl at all. periodt π€$EQUIVCPOFFS()β copies from the"PARENT"parameter at the first given offset, to the"CHILD"parameter at the second given offset π―$EQUIVCPTRUNC()β similar to the above, but if the expression given as the third parameter is false, it instead sets the"CHILD"'s value to0π€$DOCOMPALLOC()β allocates memory for any"Comp"arrays, after their size has been determined, e.g. here after$COMP(whichdims_count)has been set:
Comp => 'PDL_Indx whichdims[$COMP(whichdims_count)]',
$DOPRIVALLOC()β same as above, except the key is"Priv". because it's"Priv", this is only for entries in the"pdl_trans"itself, and almost certainly only for operations where"AffinePriv"is true π«§$SETNDIMS()β for affine transformations (specifically, the ones which setP2Childto true), this sets the child's"ndims"to the given value and allocates a suitably-sized array of dimension values π$SETDIMS()β similarly for affine transformations: after the above, and then after the actual dimension sizes are set, use this to resize the child ndarray to the right size π$SETDELTABROADCASTIDS()β similarly again, this sets the child's"nbroadcastids"to the same as the parent's, allocates space for the"broadcastids", then sets the child's ones to the same as the parent's plus the given value πͺ
to get a flavour of what "broadcastids" are for: in the normal way of things, the first (0th) one in the parent is the highest dimension-number in it. see PDL::Indexing for more π
πΈ functions πΈ
loop(DIMS) %{ ... %}β loops over named dimensions. limits are generated automatically by PP, so you literally don't have to lift a finger πbroadcastloop %{ ... %}β encloses the following code in a broadcast loop π
as of 2.075, "threadloop" is a deprecated alias for this. it still works, but it's giving legacy energy, so maybe move on bestie π«Ά
types(TYPES) %{ ... %}β executes the following code if the type of the operation is any ofTYPESβ
π Appendix C: Functions imported by PDL::PP π
a whole bunch of functions get imported when you use PDL::PP. these include functions that control the generated C or XS code, functions that control the generated Perl code, and functions that manipulate the packages and symbol tables into which the code is created. three whole vibes of function, slay π
π₯ Generating C and XS Code π₯
PDL::PP's main purpose is to make it easy for you to wrap the broadcasting engine around your own C code β but you can do some other things, too. multitasking icon π
pp_defβ used to wrap the broadcasting engine around your C code. virtually all of this document discusses the use ofpp_def. main character energy fr fr πpp_doneβ indicates you are done with PDL::PP and that it should generate its.xsand.pmfiles based upon the otherpp_*functions that you have called. this function takes no arguments. not a single one. empty-handed queen β¨pp_addxsβ this lets you add XS code to your.xsfile. useful if you want to create Perl-accessible functions that invoke C code but cannot or should not invoke the broadcasting engine. XS is the standard means by which you wrap Perl-accessible C code. you can learn more at perlxs πpp_add_bootβ adds whatever string you pass to the XS BOOT section. the BOOT section is C code that gets called by Perl when your module is loaded, and it's useful for automatic initialization. you can learn more about XS and the BOOT section at perlxs π₯Ύpp_addhdrβ adds pure-C code to your XS file. XS files are structured such that pure C code must come before XS specifications. this allows you to specify such C code π§±
π Generating Perl Code π
many functions imported when you use PDL::PP let you modify the contents of the generated .pm file. in addition to pp_def and pp_done, the role of these functions is primarily to add code to various parts of your generated .pm file π«Ά
pp_addpmβ adds Perl code to the generated.pmfile. PDL::PP actually keeps track of three different sections of generated code: the Top, the Middle, and the Bottom. you can add Perl code to the Middle section using the one-argument form, where the argument is the Perl code you want to supply. in the two-argument form, the first argument is an anonymous hash with only one key that specifies where to put the second argument, which is the string you want to add to the.pmfile. the hash is one of these three:
{At => 'Top'}
{At => 'Middle'}
{At => 'Bot'}
for example:
pp_addpm({At => 'Bot'}, <<POD);
=head1 Some documentation
I know I'm typing this in the middle of my file, but it'll go at
the bottom.
=cut
POD
Warning: if, in the middle of your.pdfile, you put documentation meant for the bottom of your pod, you will thoroughly confuse CPAN. on the other hand, if in the middle of your.pdfile you add some Perl code destined for the bottom or top of your.pmfile, you only have yourself to confuse.:-)π
pp_beginwrapβ adds BEGIN-block wrapping. certain declarations can be wrapped in BEGIN blocks, though the default behavior is to have no such wrapping πpp_addbeginβ sets code to be added to the top of your.pmfile, even above code that you specify with"pp_addpm({At => 'Top'}, ...)". unlikepp_addpm, calling this overwrites whatever was there before. generally, you probably shouldn't use it π
β Tracking Line Numbers β
when you get compile errors, either from your C-like code or your Perl code, it can help to make those errors go back to the line numbers in the source file at which the error occurred π
pp_line_numbersβ takes a line number and a (usually long) string of code. the line number should indicate the line at which the quote begins. this is usually Perl's"__LINE__"literal, unless you are using heredocs, in which case it is"__LINE__ + 1". the returned string has#linedirectives interspersed to help the compiler report errors on the proper line π§
π Modifying the Symbol Table and Export Behavior π
PDL::PP usually exports all functions generated using pp_def, and usually installs them into the PDL symbol table. however, you can modify this behavior with these functions π οΈ
pp_blessβ sets the package (symbol table) to which the XS code is added. the default isPDL, which is generally what you want. if you use the default blessing and you create a functionmyfunc, then you can do the following:
$ndarray->myfunc(<args>);
PDL::myfunc($ndarray, <args>);
on the other hand, if you bless your functions into another package, you cannot invoke them as PDL methods, and must invoke them as:
MyPackage::myfunc($ndarray, <args>);
of course, you could always use the PMFunc key to add your function to the PDL symbol table, but why do that? π€·ββοΈ
pp_add_isaβ adds to the list of modules from which your module inherits. the default list is
qw(PDL::Exporter DynaLoader)
pp_core_importlistβ at the top of your generated.pmfile is a line that looks like this:
use PDL::Core;
you can modify that by specifying a string to pp_core_importlist. for example,
pp_core_importlist('::Blarg');
will result in
use PDL::Core::Blarg;
you can use this, for example, to add a list of symbols to import from PDL::Core. for example:
pp_core_importlist(" ':Internal'");
will lead to the following use statement:
use PDL::Core ':Internal';
pp_setversionβ sets your module's version. the version must be consistent between the.xsand the.pmfile, and is used to ensure that your Perl's libraries do not suffer from version skew πpp_add_exportedβ adds to the export list whatever names you give it. functions created usingpp_defare automatically added to the list. this function is useful if you define any Perl functions usingpp_addpmorpp_addxsthat you want exported as well π€pp_export_nothingβ this resets the list of exported symbols to nothing. this is probably better called"pp_export_clear", since you can add exported symbols after calling"pp_export_nothing". when called just before callingpp_done, this ensures that your module does not export anything, for example, if you only want programmers to use your functions as methods π«
π SEE ALSO π
for the concepts of broadcasting and slicing, check PDL::Indexing π
PDL::Internals π§
PDL::BadValues for information on bad values π
perlxs, perlxstut πͺ
Practical Magick with C, PDL, and PDL::PP -- a guide to compiled add-ons for PDL https://arxiv.org/abs/1702.07753 β¨
π BUGS π
although PDL::PP is quite flexible and thoroughly used, there are surely bugs. first amongst them: this documentation needs a thorough revision. yeah, we said it π the docs are giving "finish me later" energy and honestly, same.
πΈ AUTHOR πΈ
Copyright(C) 1997 Tuomas J. Lukka (lukka@fas.harvard.edu), Karl Glaazebrook (kgb@aaocbn1.aao.GOV.AU) and Christian Soeller (c.soeller@auckland.ac.nz). All rights reserved. Documentation updates Copyright(C) 2011 David Mertens (dcmertens.perl@gmail.com). This documentation is licensed under the same terms as Perl itself π
and that's the whole page bestie, we made it to the end π«Ά this was perl v5.38.2, dated 2024-04-10, and it's the manual page PP(1p) β go forth and wrap some C code, you absolute icon ππ
β¨
π¦ where this page comes from
- ubuntu noble: pdl 1:2.085-1ubuntu1 Β· Artistic-1.0-Perl OR GPL-1.0-or-later
- π girlified by deepseek-ai/DeepSeek-V4.1-Flash on : a language model's rewrite of the original, under its terms