Table des matières
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:
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
: 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 ...