ReLoad

Thierry Jaouen ~ WikiBlog
"Rien à foutre d'être lu, tant que je peux me relire."

Outils pour utilisateurs

Outils du site


blog:2015:01:27:c_to_perl_module_simple

C to Perl Module (simple)

Pour mémoire: transformer un petit projet C en un module Perl…

~~READMORE~~

XS

L'une des méthodes pour importer une librairie C (dont on a les sources) en Perl, c'est d'utiliser l'API “XS”:
En résumé, il s'agit d'un ensemble de MACRO en C, qui surcharge l'accès aux fonctions C.

Par exemple, des macros vont transformer un “scalar” Perl, en “double” C.

Il y a aussi divers scripts a personnaliser et a completer…

foo.c etc...

Soit un ridicule projet en C :

$ cd projet_foo

Il compile:

$ test_foo.make

… et produit un “test_foo” qui donne a l'éxécution:

FIXME

Module Perl

A la fin, le module créé s’appellera “foo.pm”.

Mais il aurait pu avoir un autre nom, comme “CGI::foo.pm” .

mylib

$ mkdir perl
$ cd perl
$ mkdir foo
$ cd foo
$ mkdir libfoo
$ cd libfoo
$ ln -s ../../../foo.c .
$ ln -s ../../../foo.h .
:!: Important !
Ce sont tous les fichiers “*.c” (et “*.cpp”?) qui sont dans le répertoire “libfoo” qui seront compilés et inclus dans la lib final.

Composer un fichier “Makefile.PL” contenant:

use ExtUtils::MakeMaker;
$Verbose = 1;
WriteMakefile(
  NAME    => 'foo::libfoo',
  SKIP    => [qw(all static static_lib dynamic dynamic_lib)],
  clean   => {'FILES' => 'libfoo$(LIB_EXT)'},
  DEFINE  => '-std=gnu99',            # TJ : si besoin
);

sub MY::top_targets {
'
all :: static
pure_all :: static
static :: libfoo$(LIB_EXT)

libfoo$(LIB_EXT): $(O_FILES)
      $(AR) cr libfoo$(LIB_EXT) $(O_FILES)
      $(RANLIB) libfoo$(LIB_EXT)

';
}
### vim: set noexpandtab shiftwidth=8 softtabstop=0 : ###

Comme partout ailleurs remplacer “foo” et “libfoo” par le nom de *votre* projet.
Par ailleurs, la ligne “DEFINE => '-std=gnu99'” est spécifique a mon exemple C qui utilise certaines syntaxes “modernes” du C.

module foo.pm

Revenons au niveau du répertoire: “perl” ( “cd ../..” )

Créons le projet (de templates) pour le module “foo.pm” .

$ h2xs -O -n foo ./foo/libfoo/foo.h

FIXME: vérifier qu'ajouter “./foo/libfoo/foo.h” ne produit qu'un glish dans foo.xs avec “#include <…>”… Dans ce cas: ne pas faire.

Le répertoire “./foo/” se peuple de fichiers a personnaliser.

Ignorer le warning: “Overwriting existing foo!!!” .

$ cd foo

Maintenant, il faut dire qu'il y a une librairie, “libfoo” a générer aussi et a intégrer a ce module.

Modifier le fichier “Makefile.PL” afin d'avoir:

use 5.014002;
use ExtUtils::MakeMaker;
# See lib/ExtUtils/MakeMaker.pm for details of how to influence
# the contents of the Makefile that is written.
WriteMakefile(
    NAME              => 'foo',
    VERSION_FROM      => 'lib/foo.pm', # finds $VERSION
    PREREQ_PM         => {}, # e.g., Module::Name => 1.1
    ($] >= 5.005 ?     ## Add these new keywords supported since 5.005
      (ABSTRACT_FROM  => 'lib/foo.pm', # retrieve abstract from module
       AUTHOR         => 'Thierry Jaouen <noreply@tjaouen.fr>') : ()),
    LIBS              => [''], # e.g., '-lm'
    DEFINE            => '', # e.g., '-DHAVE_SOMETHING'
    INC               => '-I.', # e.g., '-I. -I/usr/include/other'
      # Un-comment this if you add C files to link with later:
    # OBJECT            => '$(O_FILES)', # link all the C files too
    # --- TJ ------------------
    MYEXTLIB          => 'libfoo/libfoo$(LIB_EXT)',
    # -------------------------
);
if  (eval {require ExtUtils::Constant; 1}) {
  # If you edit these definitions to change the constants used by this module,
  # you will need to use the generated const-c.inc and const-xs.inc
  # files to replace their "fallback" counterparts before distributing your
  # changes.
  my @names = (qw());
  ExtUtils::Constant::WriteConstants(
                                     NAME         => 'foo',
                                     NAMES        => \@names,
                                     DEFAULT_TYPE => 'IV',
                                     C_FILE       => 'const-c.inc',
                                     XS_FILE      => 'const-xs.inc',
                                  );

}
else {
  use File::Copy;
  use File::Spec;
  foreach my $file ('const-c.inc', 'const-xs.inc') {
    my $fallback = File::Spec->catfile('fallback', $file);
    copy ($fallback, $file) or die "Can't copy $fallback to $file: $!";
  }
}
# --- TJ ---------------------
sub MY::postamble
{
'
$(MYEXTLIB): libfoo/Makefile
      cd libfoo && $(MAKE) $(PASSTHRU)
';
}
# ----------------------------
### vim: set noexpandtab shiftwidth=8 softtabstop=0 : ###

Ajouter les fichiers dans “libfoo/” dans le “MANIFEST”. Pour moi, a la fin du fichier j'ajoute:

libfoo/Makefile.PL
libfoo/foo.c
libfoo/foo.h

Dans le fichier “foo.xs” , remplacer un “include” incorrect ( mais pourquoi ??? ) :

// --- TJ --------------
//#include <./foo/libfoo/foo.h>
#include "./libfoo/foo.h"
// ---------------------

compiler : vide

A ce stade, on doit pouvoir compiler. Dans “foo”, faire:

$ perl Makefile.PL
$ make

… mais le module est une coquille vide, car on n'a pas encore importer les fonctions C.

$ make test
...
Result: PASS
...

Le test par défaut voit bien la coquille vide.

On peut toujours tester ce vide avec un petit exemple. Soit “foo_test0” contenant:

#!/usr/bin/env perl

use ExtUtils::testlib;
use foo qw( :all );

print "Hello World\n";

prototypes

test simple 1

On va faire ça doucement…

Dans “foo.xs” , on ajoute à la fin, après la ligne “MODULE …” et/ou “INCLUDE: …” :

void
foo_void()

int
foo_void_info()

Si nécessaire:

$ make clean
$ perl Makefile.PL
$ make

Un nouveau petit script pour tester , nommé “foo_test1” :

#!/usr/bin/env perl

use ExtUtils::testlib;
use foo qw( :all );

print "Hello World\n";

foo::foo_void();
my $i = foo::foo_void_info();

print "counter=$i\n";
$ ./foo_test1
Hello World
counter=1

Mais pour l'instant, on ne peut nommer les fonctions qu'en précisant l'espace de nom auquel elles appartiennent, c'est à dire “foo::”.

Pour changer ça, il faut faire en sorte d'avoir dans “lib/foo.pm” :

our %EXPORT_TAGS = ( 'all' => [ qw(
    foo_void
    foo_void_info
) ] );

Et on recommence la compilation:

$ make clean
$ perl Makefile.PL
$ make 

Dans un nouveau script “foo_test2” , on a viré simplement les “foo::” précédent les appels de fonctions.
Et ce script devrait fonctionner.

Pour être clair, ça fonctionne aussi parce qu'on a importé “foo.pm” avec “:all”, comme ceci:

use foo qw( :all );

On aurait pu faire aussi: “use foo qw( foo_void foo_void_info )” … mais peu importe.

Pour info

Dans “foo.xs” :

int
foo_void_info()

… est équivalent a :

int
foo_void_info()
   CODE:
     RETVAL = foo_void_info()
   OUTPUT:
     RETVAL

Mais le nom des fonctions entre “Perl” et “C” étant les même, de même que le type de donnée attendu (“int”), alors on peut simplifier les déclarations dans le fichier “.xs” .

test simple 2

On ajoute a la fin de “foo.xs” :

int
foo_add(x,y)
    int x
    int y

int
foo_strlen(s)
    const char *s

void
foo_strrev(s)
    char *s

const char *
foo_strrev2(s)
    const char *s

A partir du dernier script Perl de test, on peut ajouter:

print "add=", foo_add(1,2) , "\n";
print "strlen=", foo_strlen('1234567890123456'), "\n";

my $s = 'Hello World!';
foo_strrev( $s );
print "strrev=$s\n";

print "strrev2=" , foo_strrev2( 'Hello World!' ) , "\n";

Un petit test:

Hello World
counter=1
add=3
strlen=16
strrev=!dlroW olleH
strrev2=!dlroW olleH

Notez que “strrev” modifie le contenu de la variable, alors que “strrev2” retourne une copie modifiée.

Pour info

Comme vu plus haut, les arguments échangés étant connu , on peut largement simplifié les déclarations dans “foo.xs”.

Par exemple, pour

const char *
foo_strrev2(s)
    const char *s

… est équivalent, mais en plus long de:

const char *
foo_strrev2(s)
    const char *s
  CODE:
    RETVAL = foo_strrev2( s );
  OUTPUT:
    RETVAL

test simple 3

Sur la dernière fonction C “foo_strrev2” , il y a un petit problème:

En fait, elle fait un traitement spécial dans le cas où la chaine passée en paramètre est le pointeur NULL.

Mais comment passer “NULL” ? Accordons-nous sur le fait que l'équivalent C de NULL est “undef” (not defined) en Perl.

Pour l'instant, l'appel en Perl comme ça: “foo_strrev2(undef)” ne retournera pas “undef”, mais passera en argument une chaine vide et retournera une chaine vide.

Donc, voici comment tester que le paramètre est “undefined” , et retourner “undef” lorsque la fonction C renvoi un pointeur NULL.
Pour cela, on va effectuer une nouvelle déclaration dans le fichier “foo.xs” pour avoir ça:

const char *
foo_strrev2b(s)
    const char *s
  CODE:
    RETVAL = foo_strrev2( SvOK(ST(0))? s : NULL );
    if ( RETVAL == NULL ) XSRETURN_UNDEF;
  OUTPUT:
    RETVAL

C'est fonction Perl s'appellera “foo_strrev2b” et pour la démonstration, on y accèdera via “foo::foo_strrev2b” (a moins que vous preniez la peine d'apporter de déclarer l'importation dans “lib/foo.pm” (revoir plus haut) ).

$ make clean
$ perl Makefile.PL
$ make

Enfin, un bout de code Perl a ajouter a notre script de test:

print "strrev2b=" , ( defined( foo::foo_strrev2b( undef ) ) ?"defined":"not defined" ), "\n";

Execution:

...
strrev2b=not defined

Donc, quand on passe “undef” en argument, on transmet bien NULL en argument de la fonction C.
En sortie, si la fonction C retourne NULL, on retourne “undef” en Perl.

make test

On va faire en sorte que make test effectue des tests de chaque fonction importé.

En partant ce qui a été fait ci-dessus, voici le fichier dans ”t/foo.t” :

use strict;
use warnings;

use Test::More tests => 7;
BEGIN { use_ok('foo' , qw( :all ) ) };

foo_void();

is( foo_void_info() , 1 );
is( foo_add(1,2) , 3 );
is( foo_strlen("Hello") , 5 );

my $s = 'Hello';
foo_strrev($s);

ok( $s eq 'olleH' );

ok( foo_strrev2('1234567890') eq '0987654321' );

ok( ! defined( foo::foo_strrev2b(undef) ) );

L'import du module compte pour 1 test via “use_ok(…)” .

Maintenant, on peut faire: make test, et voir:

...
All tests successful.
Files=1, Tests=7,  0 wallclock secs ( 0.01 usr  0.00 sys +  0.02 cusr  0.00 csys =  0.03 CPU)
Result: PASS
...

Sources

blog/2015/01/27/c_to_perl_module_simple.txt · Dernière modification: 2015/01/27 14:58 par thierry