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.

1 Comment

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:

IV sum_I32(SV *vals_sv) {
   STRLEN len;
   U8 *buf= SvPV(vals_sv,len);
   U8 *buf_end= buf + (len & ~0x3);
   I32 *vals= (I32 *)buf;
   I32 *vals_end= (I32 *)buf_end;
   IV sum = 0;
   for (;vals < vals_end;vals++) {
       sum += *vals;
   }
   return sum;
}

Which you then call and use like this:

my $buf= pack("l!*", @vals);
print sum_I32($buf);

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.

Leave a comment

About Steve Bertrand

user-pic Just Another Perl Hacker