http://qs321.pair.com?node_id=123361

mugwumpjism has asked for the wisdom of the Perl Monks concerning the following question:

Can anyone tell me why this XS routine returns my RETVAL with one extra depth of array refs?

#include <sys/types.h> #include <dirent.h> #include <stdio.h> SV * readdir_inode(dirname) char* dirname INIT: struct dirent *ent; DIR* dir; SV* record[2]; AV *entry, *ret_val; CODE: ret_val = newAV(); dir = opendir(dirname); if (!dir) perror("Cannot open dir"); while ((ent=readdir(dir))) { record[0] = newSVpv(ent->d_name, 0); record[1] = newSViv((IV)ent->d_ino); entry = av_make(2, record); av_push(ret_val, newRV_inc((SV*) entry)); } closedir(dir); RETVAL=newRV_inc((SV*) ret_val); OUTPUT: RETVAL

That is, I'm getting a structure like this:

$VAR1 = [ [ [ '.', 67586 ], [ '..', 255604 ], ...

Instead of something like this:

$VAR1 = [ [ '.', 67586 ], [ '..', 255604 ], ...

Any ideas why, anyone?

Also, does anyone know of a better way of saying "or die "Can't opendir $file; $!"" (or should I bother? perhaps just return undef and let the caller check $!)

Replies are listed 'Best First'.
Re: XS routine returns an extra depth of array refs
by Fastolfe (Vicar) on Nov 05, 2001 at 22:04 UTC
    Let's see your Perl code where you get the results of this function and do a Dumper on it. I expect to see:
    $ref = readdir_inode($dirname); print Dumper($ref); # or @a = readdir_inode($dirname); print Dumper($a[0]); # not Dumper(\@a)
    I don't see anything obviously wrong with your code, but I want to eliminate some assumptions first before I go digging. :)

      You hit the nail right on the head... I had the second, but Dumper \@a instead of Dumper $a[0], and my test code was assuming I was returning a list. Hate it when that happens ;-)

      OK, so now at least I have it returning something sensible, which is always good. In which case, the question is why does this:

      AV * readdir_inode(dirname) char* dirname INIT: struct dirent *ent; DIR* dir; SV* record[2]; AV *entry, *ret_val; CODE: RETVAL = newAV(); dir = opendir(dirname); if (!dir) perror("Cannot open dir"); while ((ent=readdir(dir))) { printf("%ld %x\n", ent->d_ino, ent->d_name); record[0] = newSVpv(ent->d_name, 0); record[1] = newSViv((IV)ent->d_ino); entry = av_make(2, record); av_push(RETVAL, newRV_inc((SV*) entry)); } closedir(dir); /*RETVAL=*ret_val;*/ /*RETVAL=newRV_inc((SV*) ret_val);*/ OUTPUT: RETVAL

      Return an extra level of arrays?

      The calling code:

      my @ents = readdir_inode("."); print Dumper @ents;

      The output:

      $VAR1 = [ [ '.', 67586 ], [ '..', 255604
      ...
        When you use RETVAL, XS builds it in such a way that it's functionally the same as the first item on the return stack. Thus, you've effectively stuck an AV into $ents[0], which I'm guessing Perl DWIM's here and turns it into a reference to an array. Hence the additional level of references.

        The correct way (I believe) to return a list in this situation is to declare your function with a return type of void and simply push each element onto the return stack instead of trying to return an array, using PUSHs I think.

Re: XS routine returns an extra depth of array refs
by Fastolfe (Vicar) on Nov 05, 2001 at 22:09 UTC
    Returning 'undef' from your function on error and setting $! seems the best way of doing this. If you really want to throw an error directly from your XS, use the "croak" function, which behaves like die in Perl. (There's also a 'warn'.) These act like printf.
    croak("Can't opendir %s!", dirname);
Re: XS routine returns an extra depth of array refs
by jeroenes (Priest) on Nov 05, 2001 at 22:28 UTC
    Maybe the casting of entry to an SV does the trick. newRC_inc can also take an AV as input.

    Update: Tested the following... too lazy for XS, so Inline:

    use Data::Dumper; $ref = testRV( 'monks' ); print Dumper( $ref ); use Inline C => <<'END_OF_C' SV* testRV(char* dirname){ SV* record[2]; AV *entry, *ret_val; SV* retarray; int i = 0; ret_val = newAV(); while (i<10) { record[0] = newSVpv(dirname, 0); record[1] = newSViv(i); entry = av_make(2, record); av_push(ret_val, newRV_inc( entry)); i++; } retarray=newRV_noinc( ret_val); return retarray; } END_OF_C #results in: $VAR1 = [ [ 'monks', '0' ], [ 'monks', 1 ], [ 'monks', 2 ], ...
    In other words, just a 2D array as you wanted. BTW, Inline didn't complain about the AV* 's, but than, maybe those warnings were suppressed?

    So I think Fastolfe has pinpointed it, when he noticed you exc'd the sub in array context...

    If you really want to output arrays from C, you need to push your items on the stack.

      If you really want to output arrays from C, you need to push your items on the stack.

      Right, that's the ticket. This seems to work the way I want:

      #include <sys/types.h> #include <dirent.h> #include <stdio.h> void readdir_inode(dirname) char* dirname INIT: struct dirent *ent; DIR* dir; SV* record[2]; AV *entry, *ret_val; PPCODE: dir = opendir(dirname); if (dir) { while ((ent=readdir(dir))) { printf("%ld %x\n", ent->d_ino, ent->d_name); record[0] = newSVpv(ent->d_name, 0); record[1] = newSViv((IV)ent->d_ino); PUSHs(newRV_noinc(av_make(2, record))); } closedir(dir); }

      At least, I hope that last "newRV_noinc" shouldn't be a "newRV_inc"!

        It should be newRV_inc. :-)

        Only with inline the refcounter of the SV gets auto-incremented. With XS you have to that yourself.

        Update: For clarity, only the refcounter of the SV that gets returned! Other SV*'s you have to inc manually!

      All the casting does is remove this warning during compilation:

      ReadDir.xs: In function `XS_ReadDir_readdir_inode': ReadDir.xs:43: warning: passing arg 1 of `Perl_newRV' from incompatibl +e pointer type