Recommend Me


Jeudi 19 janvier 2006

Ecrire des modules dynamiques PHP en C

(Linux Magazine France n°20 - Septembre 2000 - sources)

PHP est un langage relativement jeune. Il ne peut pas encore se vanter de posséder une librairie de fonctions aussi fournie que des langages tel que Perl, Python… Mais avec quelques bases de C et une bonne nuit blanche à étudier l’API PHP vous devriez pouvoir combler les lacunes.

1. Qu’est-ce qu’un Module Dynamique ?

Un Module Chargé Dynamiquement (Dynamic Loadable Modules ou DLM en English) est un ensemble de fonctions écrites dans un langage annexe (le C ici). Le DLM peut être chargé, à la demande, par PHP afin d’étendre les fonctionnalités de ce dernier.

Imaginons, par exemple, que vous souhaitiez traiter des fichier XML avec PHP. Je sais, il existe déjà des fonctions dans PHP pour ça. Cependant vous pouvez être amenés à vouloir faire des traitements un peu plus complexes que ceux que proposent les fonctions fournies avec PHP, comme par exemple utiliser XPath. Vous avez alors deux solutions: ou vous réinventez la roue et reprogrammez l’ensemble des fonctions nécessaires en PHP directement; ou vous récupérez une librairie existante (libxml[1] par exemple) sur le Oueb et vous l’interfacez avec PHP.

Dans le second cas, vous en conviendrez, la plus grosse partie du travail est déjà faite.

2. l’API PHP4

Si vous avez déjà été tenté par l’aventure, peut-être avez-vous déjà jeté un oeil sur les fichiers “apidoc.txt” et “apidoc-zend.txt” fournis avec les sources de PHP4. Étonné, puis déçu vous avez été ! En effet ces deux documents ne traitent pas de l’API PHP4, mais celle de PHP3. Cependant L’API PHP4 est relativement semblable à celle de PHP3. Seuls quelques noms de fonctions et structures ont changé et quelques macros supplémentaires ont vu le jour. Notons aussi que, étrangement, quelques fonctions ont disparu !!!

3. Notre premier module

J’étais tenté de décrire, comme premier exemple, un module de type “Hello World”. Mais développer quelque chose d’encore plus inutile est plus amusant. Imaginons donc une fonction qui prend en entrée une chaîne de caractères et la retourne.

Le programme de test de notre module sera le suivant :

test1.php

<?php
# On charge le module
dl( ‘php_text.so’ );

# On appelle notre nouvelle fonction
$retour = text( ‘Hello World !’ );
   
# On affiche le resultat
echo $retour; // Hello World !
?>

Écrivons donc notre module “php_text”.

La première chose à faire est de déclarer les points d’entrée. Pour ce premier exemple nous avons donc à déclarer la fonction “text” comme nouvelle fonction PHP. Ceci se fait par l’intermédiaire de deux structures :

php_text.c

/*
 * Points d’entree du module
 */

zend_function_entry php_text_functions[] = {
  PHP_FE(text, NULL)
  {NULL, NULL, NULL}
};
 
zend_module_entry php_text_module_entry = {
  "php_text",
  php_text_functions,
  NULL, NULL, NULL, NULL, NULL,
  STANDARD_MODULE_PROPERTIES,
};

Dans la structure de type “zend_function_entry” on donne le nom de la nouvelle fonction PHP : “text”. Dans la structure de type “zend_module_entry” on a spécifié les nouveaux points d’entrée. Je ne détaillerai pas plus ces deux structures. Je vous laisse le soin d’aller jeter un oeil dans les includes zend.h et modules.h des sources de PHP pour en savoir plus. Sachez seulement que la macro PHP_FE permet de définir une fonction PHP et le type de ses paramètres…

On va ensuite faire prendre en compte ces points d’entrée par PHP :

php_text.c (suite)

#if COMPILE_DL
DLEXPORT zend_module_entry *get_module(void)
{ return &php_text_module_entry; }
#endif

Voila, il ne reste plus qu’à écrire notre fonction “text”.

Toute fonction PHP qui doit être exportée par le module se déclare de la façon suivante :

php_text.c (suite)

DLEXPORT PHP_FUNCTION(text)
{
  /** Arguments de la fonction PHP */
  zval **arg;

où ici “text” est le nom de notre fonction et “arg” est l’argument passé. Cet argument (de type zval **) sera par la suite converti afin de récupérer son contenu. Mais avant cela nous allons vérifier qu’il y a bien eu un argument passé depuis le PHP. En effet on ne veut pas d’appel de la fonction “test” sans argument et si tel n’est pas le cas, on va laisser PHP gérer l’erreur de tel sorte que si on écrit

<?php
# …
$retour = text();
# …
?>

PHP renvoie le message

    Warning: Wrong parameter count for text() in php_text_test.php on line 5

On ajoute donc les lignes suivantes :

php_text.c (suite)

  /** Verification des parametres */
  if (zend_get_parameters_ex( 1, &arg ) != SUCCESS) {
    WRONG_PARAM_COUNT;
  }

En fait, en plus de vérifier la présence des paramètres la fonction “zend_get_parameters_ex” permet de les récupérer. En effet c’est elle qui va permettre d’affecter la variable “arg”. D’ou son importance ! Comme je l’ai dit plus haut, il est nécessaire de convertir notre paramètre d’entrée afin de récupérer son contenu. Pour cela l’API PHP met à notre disposition différentes fonctions :

  • convert_to_long / convert_to_long_ex
  • convert_to_long_base / convert_to_long_base_ex
  • convert_to_double / convert_to_double_ex
  • convert_to_string / convert_to_string_ex
  • convert_to_boolean / convert_to_boolean_ex
  • convert_to_array / convert_to_array_ex
  • convert_to_object / convert_to_object_ex
  • convert_to_null / convert_to_null_ex

Dans notre cas on traite une chaîne de caractères :

php_text.c (suite)

  /** Conversion de l’argument */
  convert_to_string( *arg );

( Pour ceux qui se posent la question, convert_to_string( *arg ); est identique à convert_to_string_ex( arg ); ) Si nous avons, au préalable, déclaré un tableau de caractères (sResult) on peut y recopier maintenant le contenu de arg :

php_text.c (suite)

  /** Chaine renvoyee */
  char sResult[256];

  // …

   /** Copie de la chaine */
  sprintf( sResult, (*arg)->value.str.val );

  RETURN_STRING( sResult, 1 );
}

RETURN_STRING permet ici de spécifier que l’on renvoie la chaîne “sResult”. Comme vous pourrez le voir plus bas ce n’est (heureusement) pas le seul type de variable que l’on peut renvoyer.

Voilà pour le module. Cependant, avant de se lancer dans la compilation, il y a une chose très importante que nous n’avons pas faite : écrire le prototype de notre nouvelle fonction. Pour la beauté du geste on fera un fichier php_text.h :

php_text.h

/*
 * php_text.so
 * Grégoire Lejeune <glejeune@aurora-linux.com>
 *
 * Prototypes des fonctions accessibles depuis PHP
 */

#ifndef _INCLUDED_TEXT_H
#define _INCLUDED_TEXT_H

#include "phpdl.h"

/* Functions accessibles depuis PHP */
DLEXPORT PHP_FUNCTION(text);

#endif

et on rajoutera en début de php_text.c :

php_text.c (début)

/*
 * php_text.so
 * Grégoire Lejeune <glejeune@aurora-linux.com>
 *
 * Exemple de module de gestion de chaînes de caracteres
 */

#include "php_text.h"

Comme vous pouvez le voir à la ligne 11 de php_text.h, on inclut le fichier phpdl.h. Recopiez-le depuis le répertoire des sources de PHP dans le répertoire contenant les deux fichiers php_text.c et php_text.h. Écrivons un Makefile (modifier les lignes 15 et 16) :

#
# Makefile pour php_text.so
# Grégoire Lejeune <glejeune@aurora-linux.com>
#

## Nom du module
MODULE = php_text.so

## Objects associes
CAL_OBJS =  php_text.o

## Includes
INCLUDE = -I. \
  -I/usr/local/include \
  -I/path/to/php \
  -I/path/to/php/Zend

## Libs
LIB = -L/usr/local/lib

## CFLAGS
CFLAGS =

## Compile
CC = cc -O2 -Dbool=char -DHAS_BOOL -D_REENTRANT $(INCLUDE) -fPIC
LD = cc -shared $(LIB) -rdynamic

## ————————————————————

all: $(MODULE)

$(MODULE): $(CAL_OBJS)
  $(LD) -o $@ $(CAL_OBJS)

%.o: %.c
  $(CC) $(CFLAGS) -DCOMPILE_DL=1 -c -o $@ $<

clean:
  -rm -f *.so *.o

Et testez… Formidable non ?

4. Convertir les arguments

Dans l’exemple que nous venons de voir nous avons récupéré une chaîne de caractères. Pour cela nous avons converti notre argument en “string” et avec un (*arg)->value.str.val nous avons eu accès à sa valeur. Si nous voulons récupérer un long (par exemple) on utilisera la méthode suivante :

// …
convert_to_long_ex( number );
// …
mon_long = (*number)->value.lval;
// …

Vous pouvez regarder l’exemple “php_number” présent sur le CDROM du magazine.

Pour savoir quel est le champ de la structure “zval” contenant la valeur que l’on souhaite récupérer, le plus simple est de connaître le contenu de cette structure (Cf. zend.h) :

struct _zval_struct {
    /* Variable information */
    zvalue_value value;     /* value */
    zend_uchar type;    /* active type */
    zend_uchar is_ref;
    zend_ushort refcount;
};

Comme on le voit le champ “type” va nous donner le… type (merci ;). Celui-ci peut être égal à :

  • IS_STRING
  • IS_LONG
  • IS_DOUBLE
  • IS_ARRAY
  • IS_EMPTY
  • IS_USER_FUNCTION
  • IS_INTERNAL_FUNCTION
  • IS_CLASS
  • IS_OBJECT

Les quatre premiers étant ceux que vous avez le plus de chance d’utiliser. à quoi correspond la structure zvalue_value (Cf. zend.h) :

typedef union _zvalue_value {
    long lval;                  /* long value */
    double dval;                /* double value */
    struct {
        char *val;
        int len;
    } str;
    HashTable *ht;              /* hash table value */
    struct {
        zend_class_entry *ce;
        HashTable *properties;
    } obj;
} zvalue_value;

Bon, je pense que vous avez compris :

  • (…)->value.lval pour récupérer un long,
  • (…)->value.dval pour récupérer un double,
  • (…)->value.str.val pour récupérer un string
  • (…)->value.ht pour récupérer un tableau (hashage)

Pour les tableaux, si vous jetez un oeil dans zend_hash.h vous comprendrez vite que c’est un peu plus complexe. En effet, on s’en doute, value.HashTable ne pointe pas directement vers le contenu de notre tableau :

typedef struct hashtable {
    uint nTableSize;
    uint nHashSizeIndex;
    uint nNumOfElements;
    ulong nNextFreeElement;
    hash_func_t pHashFunction;
    Bucket *pInternalPointer;   /* Used for element traversal */
    Bucket *pListHead;
    Bucket *pListTail;
    Bucket **arBuckets;
    dtor_func_t pDestructor;
    unsigned char persistent</nowiki>;
#if ZEND_DEBUG
    int inconsistent;
#endif
} HashTable;

typedef struct bucket {
    ulong h;                        /* Used for numeric indexing */
    uint nKeyLength;
    void *pData;
    void *pDataPtr;
    struct bucket *pListNext;
    struct bucket *pListLast;
    struct bucket *pNext;
    struct bucket *pLast;
    char arKey[1]; /* Must be last element */
} Bucket;

En fait il suffit de savoir comment parcourir ce tableau. L’exemple le plus simple est donné dans zend_hash.c avec la fonction zend_hash_display :

void zend_hash_display(HashTable *ht)
{
    Bucket *p;
    uint i;

    for (i = 0; i < ht->nTableSize; i++) {
        p = ht->arBuckets[i];
        while (p != NULL) {
            zend_printf("%s <==> 0x%X\n", p->arKey, p->h);
            p = p->pNext;
        }
    }

    p = ht->pListTail;
    while (p != NULL) {
        zend_printf("%s <==> 0x%X\n", p->arKey, p->h);
        p = p->pListLast;
    }
}

Donc pour i allant de 0 à ((…)->value.ht)->nTableSize,

  • ((…)->value.ht)->arBuckets[i]->arKey donne la clé pour le ième élément,
  • ((…)->value.ht)->arBuckets[i]->h donne l’index
  • ((…)->value.ht)->arBuckets[i]->pData donne le pointeur sur le contenu.

Ok, ok, ok… Tout ça c’est un peu imbuvable… L’exemple “php_array2″ sera sans doute plus parlant.

5. Retourner des valeurs

Dans php_text.c nous avons utilisé la macro RETURN_STRING pour renvoyer notre chaîne. Il existe d’autres macros de type RETURN_XXX permettant de retourner des éléments de type simple :

  • RETURN_LONG( value );
  • RETURN_DOUBLE( value );
  • RETURN_STRING( value, 1 );
  • RETURN_STRINGL( value, Length, 1 );
  • RETURN_TRUE;
  • RETURN_FALSE;

Mais il est aussi possible de retourner des tableaux ou des objets. Je ne m’étendrai pas sur les objets. En effet entre PHP3 et PHP4 l’API a perdu la possibilité de retourner un objet contenant autre chose que des attributs !!! Exit les méthodes !!! ARG ! ( Ceci est corrige avec la version 4.0.1, mais je ne m’etendrais pas sur le sujet. Regardez l’exemple fournis dans les sources. )

Pour les tableaux, il n’y a pas de macro RETURN_XXX. Il suffit de créer un nouveau tableau :

// …
if( array_init( return_value ) == FAILURE ) {
  php_error( E_ERROR, "Erreur d’initialisation du tableau." );
}
// …

Et de le remplir en utilisant les fonctions d’ajout d’élément :

  • add_index_long( return_value, index, long_value );

    add_next_index_long( return_value, long_value );

    add_assoc_long( return_value, key, long_value );
  • add_index_double( return_value, index, double_value );

    add_next_index_double( return_value, double_value );

    add_assoc_double( return_value, key, double_value );
  • add_index_string( return_value, index, string_value );

    add_next_index_string( return_value, string_value );

    add_assoc_string( return_value, key, string_value );
  • add_index_stringl( return_value, index, string_value, length );

    add_next_index_stringl( return_value, string_value, length );

    add_assoc_stringl( return_value, key, string_value, length );

où les fonctions add_index_xxx permettent d’ajouter un élément de type “xxx” à un index donné, add_next_index_xxx de rajouter un élément de type “xxx” en fin de tableau et add_assoc_xxx de rajouter un élément de type “xxx” dans un hashage avec la clé “key”.

exemple :

// …
add_next_index_long( return_value, (*my_long)->value.lval );
// …

6. Créer des variables globales

La dernière chose à connaître de l’API PHP est la possibilité de créer des variables globales. Ceci peut être fait grâce aux macros suivantes :

  • SET_VAR_STRING( name, value );
  • SET_VAR_STRINGL( name, value );
  • SET_VAR_LONG( name, value, length );
  • SET_VAR_DOUBLE( name, value );

ATTENTION, dans le cas de SET_VAR_STRING et SET_VAR_STRINGL la valeur doit avoir été allouée.

Prenons comme exemple un fonction PHP dans laquelle on veut créer une variable TEXT dans laquelle on va stocker… un texte ;). On reprend notre premier module et on le modifie :

 /** Contenu de la variable Globale cree */
  char *psContent;
  // …
   /** On verifie si la variable TEXT n’existe pas deja */
  if ( zend_hash_exists( &EG(symbol_table), "TEXT", sizeof( "TEXT" ) ) ) {
    php_error( E_WARNING, "TEXT already exists" );
  } else {
    /** On alloue l’espace memoire pour psContent */
    psContent = (char *)malloc( sizeof( char )*(
                         strlen( (*arg)->value.str.val )+1 ) );

    /** Conversion de l’argument */
    convert_to_string( *arg );

    /** Copie du contenu de l’argument */
    strcpy( psContent, (*arg)->value.str.val );

    /** Creation de la variable TEXT */
    SET_VAR_STRING( "TEXT", psContent );
  }

Conclusion

Il y aurait encore beaucoup à dire sur l’API PHP. Mais comme je ne suis pas là pour écrire un livre je vous laisse le soin de pousser plus loin. Vous trouverez, dans les sources fournis avec cet article, un exemple de module montrant comment renvoyer des objects, un autre sur l’appel de fonction utilisateur… En attendant vous êtes déjà capable avec celà d’écrire des modules pour PHP. Let’s imagine…

• • •

Pas de commentaire »

Pas encore de commentaire.

RSS des commentairesTrackBack URI

Laisser un commentaire

You must be logged in to post a comment.

Powered by: WordPress • Template adapted from the Simple Green' Wench theme - RSS