Développement d’extensions PHP : concepts fondamentaux et bonnes pratiques

PHP, bien qu’interprété au niveau utilisateur, repose sur une couche C profonde où la gestion mémoire devient critique. À ce stade, la sécurité n’est plus garantie par le moteur — elle dépend entièrement de la rigueur des développeurs impliquant des allocations, des libérations et des accès valides aux structures internes.

Sécurité multithread et macros TSRM

Dans les environnements threadés (ZTS), l’accès aux données globales doit être isolé par contexte. Le système TSRM fournit des macros pour gérer ce partage en toute sécurité :

#define TSRMLS_FETCH() void ***tsrm_ls = (void ***)ts_resource_ex(0, NULL)
#define TSRMLS_D void ***tsrm_ls
#define TSRMLS_DC , TSRMLS_D
#define TSRMLS_C tsrm_ls
#define TSRMLS_CC , TSRMLS_C

Ces macros suivent une logique cohérente : TSRMLS_D déclare le pointeur de contexte dans la signature de fonction, TSRMLS_DC l’ajoute avec une virgule ; TSRMLS_C et TSRMLS_CC le passent comme argument lors de l’appel. Par exemple :

int process_data(uint32_t id, const char *input TSRMLS_DC);
process_data(101, "payload" TSRMLS_CC);

L’appel à TSRMLS_FETCH() coûte en performance. Il est donc recommandé de l’invoquer une seule fois par portée, impérativement avant toute autre instruction — notamment pour compatibilité avec les compilateurs C++.

Cycle de vie d’un module PHP

Qu’il s’exécute en mode CLI ou via un SAPI web (Apache, FPM), chaque module PHP traverse des phases bien définies :

  • PHP_MINIT_FUNCTION : initialisation statqiue du module (chargement, registre de fonctions)
  • PHP_MSHUTDOWN_FUNCTION : nettoyage global (libération de ressources persistantes)
  • PHP_RINIT_FUNCTION : préparation par requête (ex. initialisation de variables locales)
  • PHP_RSHUTDOWN_FUNCTION : finalisation par requête (fermeture temporaire, vidage de buffers)
  • PHP_GINIT_FUNCTION / PHP_GSHUTDOWN_FUNCTION : gestion des variables globales thread-safe
  • PHP_MINFO_FUNCTION : affichage des informations dans phpinfo()

Voici un exemple minimaliste illustrant l’enregistrement temporel des événements :

static time_t module_start;
static time_t request_start;

PHP_MINIT_FUNCTION(sample) {
    module_start = time(NULL);
    return SUCCESS;
}

PHP_RINIT_FUNCTION(sample) {
    request_start = time(NULL);
    return SUCCESS;
}

PHP_MSHUTDOWN_FUNCTION(sample) {
    FILE *log = fopen("/tmp/module_shutdown.log", "a");
    fprintf(log, "Shutdown at %ld\n", time(NULL));
    fclose(log);
    return SUCCESS;
}

Débogage des erreurs de segmentation

Sur Linux, une erreur d’accès mémoire déclenche un segmentation fault. Si le noyau est configuré pour générer des fichiers core, ceux-ci permettent une analyse post-mortem précise avec gdb :

  1. Vérifier la limite actuelle : ulimit -c
  2. Activer les dumps illimités : ulimit -c unlimited
  3. Exécuter le script défaillant (ex. php crash.php)
  4. Identifier le fichier core.* généré
  5. Analyser avec gdb /usr/bin/php core.12345

Pour rendre cela permanent, ajouter ulimit -c unlimited dans ~/.bashrc ou /etc/profile. Le format des noms de fichiers core peut aussi être personnalisé via /proc/sys/kernel/core_pattern.

Macros d’accès aux structures globales

Le cœur PHP expose plusieurs espaces de données via des macros abstraites :

  • SG(v) → Accès aux données SAPI (sapi_globals_struct) : URI, en-têtes, paramètres
  • EG(v) → Environnement d’exécution (_zend_execution_globals) : tables de symboles, pile d’appels
  • CG(v) → Données de compilation (fonctions prédéfinies, opcodes)
  • PG(v) → Configuration issue de php.ini (ex. memory_limit)
  • FG(v) → État spécifique aux extensions standard (gestion de fichiers, flux)

Exemple concret d’accès à l’URI courante :

const char *uri = SG(request_info).request_uri;

Manipulation des zval et typage

Chaque valeur PHP est encapsulée dans une structure zval. L’accès aux types et contenus se fait via des macros spécialisées :

Type Macro (zval) Macro (zval*) Macro (zval**)
Entier Z_LVAL(z) Z_LVAL_P(p) Z_LVAL_PP(pp)
Chaîne Z_STRVAL(z)/Z_STRLEN(z) Z_STRVAL_P(p)/Z_STRLEN_P(p) Z_STRVAL_PP(pp)/Z_STRLEN_PP(pp)
Table de hachage Z_ARRVAL(z) Z_ARRVAL_P(p) Z_ARRVAL_PP(pp)

La fonction gettype() illustre leur usage :

PHP_FUNCTION(gettype) {
    zval **input;
    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "Z", &input) == FAILURE) {
        RETURN_FALSE;
    }

    switch (Z_TYPE_PP(input)) {
        case IS_NULL:     RETURN_STRING("NULL", 1); break;
        case IS_BOOL:     RETURN_STRING("boolean", 1); break;
        case IS_LONG:     RETURN_STRING("integer", 1); break;
        case IS_DOUBLE:   RETURN_STRING("double", 1); break;
        case IS_STRING:   RETURN_STRING("string", 1); break;
        case IS_ARRAY:    RETURN_STRING("array", 1); break;
        case IS_OBJECT:   RETURN_STRING("object", 1); break;
        default:          RETURN_STRING("unknown", 1);
    }
}

Déclaration de constantes et variables globales

Les constantes sont enregistrées durant l’initialisation du module ou à chaque requête :

PHP_MINIT_FUNCTION(myext) {
    REGISTER_LONG_CONSTANT("MYEXT_VERSION", 2024, CONST_CS | CONST_PERSISTENT);
    REGISTER_STRING_CONSTANT("MYEXT_NAME", "engine", CONST_CS);
    return SUCCESS;
}

PHP_RINIT_FUNCTION(myext) {
    char rand_id[16];
    snprintf(rand_id, sizeof(rand_id), "%u", (unsigned int)rand());
    REGISTER_STRING_CONSTANT("MYEXT_SESSION_ID", estrdup(rand_id), CONST_CS);
    return SUCCESS;
}

Pour les variables superglobalse ($_GET, $_SERVER), PHP les stocke dans la table de symboles globale accessible via &EG(symbol_table). Un accès typique à $_SERVER['HTTP_USER_AGENT'] suit ce schéma :

zval **server, **ua;
if (zend_hash_find(&EG(symbol_table), "_SERVER", sizeof("_SERVER"), (void **)&server) == SUCCESS &&
    Z_TYPE_PP(server) == IS_ARRAY) {
    HashTable *ht = Z_ARRVAL_PP(server);
    if (zend_hash_find(ht, "HTTP_USER_AGENT", sizeof("HTTP_USER_AGENT"), (void **)&ua) == SUCCESS) {
        RETVAL_STRINGL(Z_STRVAL_PP(ua), Z_STRLEN_PP(ua), 1);
    }
}

Intégration de bibliothèques externes

Pour lier une extension à une bibliothèque comme cURL, le script config.m4 doit détecter les chemins d’en-têtes et de bibliothèques :

PHP_ARG_WITH(curl, for cURL support,
[  --with-curl[=DIR]      Include cURL support])

if test "$PHP_CURL" != "no"; then
  SEARCH_PATH="/usr/local /usr"
  for i in $SEARCH_PATH; do
    if test -f "$i/include/curl/curl.h"; then
      CURL_DIR=$i
      PHP_ADD_INCLUDE($CURL_DIR/include)
      break
    fi
  done

  if test -z "$CURL_DIR"; then
    AC_MSG_ERROR([cURL headers not found.])
  fi

  PHP_CHECK_LIBRARY(curl, curl_global_init,
    [PHP_ADD_LIBRARY_WITH_PATH(curl, $CURL_DIR/lib, CURL_SHARED_LIBADD)],
    [AC_MSG_ERROR([cURL library not found.])],
    [-L$CURL_DIR/lib -lcurl]
  )
  PHP_SUBST(CURL_SHARED_LIBADD)
fi

Retours de fonctions et gestion des zval

Les macros RETURN_* simplifient la sortie de valeurs depuis une fonction C :

PHP_FUNCTION(add_numbers) {
    long a, b;
    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "ll", &a, &b) == FAILURE) {
        RETURN_FALSE;
    }
    RETURN_LONG(a + b);
}

PHP_FUNCTION(return_array) {
    zval *arr;
    MAKE_STD_ZVAL(arr);
    array_init(arr);
    add_assoc_long(arr, "sum", 42);
    add_assoc_string(arr, "status", "ok", 1);
    RETURN_ZVAL(arr, 1, 1);
}

Parcours des tableaux (HashTable)

Les tableaux PHP sont implémentés comme des HashTable. Leur itération sécurisée utilise des pointeurs internes :

HashTable *ht = Z_ARRVAL_P(input_array);
HashPosition pos;
zval **entry;

for (zend_hash_internal_pointer_reset_ex(ht, &pos);
     zend_hash_get_current_data_ex(ht, (void**)&entry, &pos) == SUCCESS;
     zend_hash_move_forward_ex(ht, &pos)) {
    
    if (Z_TYPE_PP(entry) == IS_STRING) {
        php_printf("String: %s\n", Z_STRVAL_PP(entry));
    }
}

Des fonctions comme zend_hash_num_elements() ou zend_hash_index_update() permettent également des opérations directes sur les clés numériques ou chaînes.

Étiquettes: php-extension zend-engine C-Language tsrm hash-table

Publié le 7 août à 21h05