Send in a Perl aref to C, get back a Perl array (and using the generated XS)
This is a tutorial as much as it is a request for guidance from experienced XS/C/perlguts folks, as TIMTOWTDI, and in this case, likely, a better way.
This will show you how to pass a Perl array reference (aref) into a C function, convert the aref into a C array, work on it, then push it back onto the stack so the C function returns it as a Perl array.
It'll also show that although we bite off of
Inline::C,
the XS code it generates can be used in your distribution, even without the end-user needing Inline
installed.
First, straight to the code. Comments inline for what's happening (or, at least, what I think is happening... feedback welcomed):
use warnings;
use strict;
use feature 'say';
use Inline 'Noclean';
use Inline 'C';
my $aref = [qw(1 2 3 4 5)];
# overwrite the existing aref to
# minimize memory usage
@$aref = aref_to_array($aref);
say $_ for @$aref;
__END__
__C__
void aref_to_array(SV* aref){
// check if the param is an array reference...
// die() if not
if (! SvROK(aref) || SvTYPE(SvRV(aref)) != SVt_PVAV){
croak("not an aref\n");
}
// convert the array reference into a Perl array
AV* chars = (AV*)SvRV(aref);
// allocate for a C array, with the same number of
// elements the Perl array has
unsigned char buf[av_len(chars)+1];
// convert the Perl array to a C array
int i;
for (i=0; i<sizeof(buf); i++){
SV** elem = av_fetch(chars, i, 0);
buf[i] = (unsigned char)SvNV(*elem);
}
// prepare the stack
inline_stack_vars;
inline_stack_reset;
int x;
for (x=0; x<sizeof(buf); x++){
// extract elem, do stuff with it,
// then push to stack
char* elem = buf[x];
elem++;
inline_stack_push(sv_2mortal(newSViv(elem)));
}
// done!
inline_stack_done;
}
We now
get an _Inline
directory created within the current working directory, which
has a build/
dir and then a sub directory (or multiple, just look at the one with the most recent timestamp). Peek in there, and you'll see a
file with an .xs
extention. This is the file you want if you want to include
your work into a real Perl distribution. This essentially allows one to utilize my favourite feature of Inline::C
, which is to build
XS code for us, without having to know any XS (or little XS) at all.
After I run the above example, I get this in the XS file (my comments removed):
#include "EXTERN.h"
#include "perl.h"
#include "XSUB.h"
#include "INLINE.h"
void aref_to_array(SV* aref){
if (! SvROK(aref) || SvTYPE(SvRV(aref)) != SVt_PVAV){
croak("not an aref\n");
}
AV* chars = (AV*)SvRV(aref);
unsigned char buf[av_len(chars)+1];
int i;
for (i=0; i<sizeof(buf); i++){
SV** elem = av_fetch(chars, i, 0);
buf[i] = (unsigned char)SvNV(*elem);
}
inline_stack_vars;
inline_stack_reset;
int x;
for (x=0; x<sizeof(buf); x++){
char* elem = buf[x];
elem++;
inline_stack_push(sv_2mortal(newSViv(elem)));
}
inline_stack_done;
}
MODULE = c_and_back_pl_f8ff PACKAGE = main
PROTOTYPES: DISABLE
void
aref_to_array (aref)
SV * aref
PREINIT:
I32* temp;
PPCODE:
temp = PL_markstack_ptr++;
aref_to_array(aref);
if (PL_markstack_ptr != temp) {
/* truly void, because dXSARGS not invoked */
PL_markstack_ptr = temp;
XSRETURN_EMPTY; /* return empty stack */
}
/* must have used dXSARGS; list context implied */
return; /* assume stack size is correct */
To note is the following line:
MODULE = c_and_back_pl_f8ff PACKAGE = main
That dictates the name of the module you're creating the XS for. You'll want to change it to something like:
MODULE = My::Module PACKAGE = My::Module
...then put that file in the root of your distribution, and add, into your
distributions primary .pm
module file:
require XSLoader;
XSLoader::load('My::Module', $VERSION);
Normally, the INLINE.h
include
can be removed, but because we're using some
Inline
functionality, we need to grab a copy of INLINE.h
from somewhere and
copy it into the root directory of our distribution so that everything compiles
nicely. There's always a copy of it in the _Inline/build/*
directory
mentioned above. Providing this header file will allow users of your
distribution that don't have Inline::C
installed to use your module as if
they did have it.
Your XS code will be more efficient if you bypass Inline::C and use XS directly. On the other hand, the strategy you outlined is a good way to learn XS in the first place.
Also, there are modules out there to help you pass structs, especially arrays, back and forth between C and Perl.
Also, often it is easier to *not* do this kind of code in C, but rather let pack() do it.
So for instance, you can write a C function like:
Which you then call and use like this:
Vice versa, if you have a C array in C that you want to return to Perl, you can return it as a U8* and then unpack it in Perl. (Note the '!' is important, as it tells pack to use the native format for the type, so it will DWIM regardless of architecture type.