PHP FFI : passage de paramètres – partie 2

Appeler directement la librairie php ?

Et si on s’amusait à réaliser un bout de code qui ne sert à rien ? Allez !!! Allez !! Allez !!!

Bon OK. Je vous propose comme truc inutile d’utiliser PHP pour appeler une lib en C qui utilise le Zend Engine.

En réalité ce paragraphe n’est pas aussi inutile que cela. Son but est de vous montrer un comportement particulier 🙂

PHP-ffi a quelques limitations : impossible de lui passer une variable PHP directement pour récupérer sa valeur dynamiquement depuis la lib externe.

Vous n’avez accès qu’au passage de type de données du langage C. La vraie bonne façon de passer des paramètres et de récupérer les valeurs est via :

$var = FFI::new('int'); 
$var->cdata=33; 

Il en sera de même pour récupérer les données.
Vous trouverez plus d’infos sur le site officiel de PHP : https://www.php.net/manual/fr/class.ffi-cdata.php
Nous y reviendrons prochainement.

Cela étant, dans cet exemple, nous allons utiliser zend pour récupérer la valeur d’une variable directement via son nom. Ceci ne peut marcher que si la variable se trouve dans son contexte. Bon, sachez le, cette pratique ne sert strictement à rien. Mais elle nous permet d’introduire comment utiliser le Zend Engine si jamais on en avait besoin.

On va commencer par installer g++ et les header files de php 8.1

sudo apt-get install g++ php8.1-dev

Voici donc notre code en C qui va nous permettre d’accéder à nos différentes valeurs :

extern "C" {
    #include "php.h"
}

// FROM : https://stackoverflow.com/questions/70771433/is-it-possible-to-pass-php-variable-to-ffi-c-function
// https://github.com/mrsuh/php-var-sizeof/blob/master/library/ffi.cpp
zval * get_zval_by_name(char * name) {

    HashTable *symbol_table = zend_array_dup(zend_rebuild_symbol_table());

    zend_string *key_name = zend_string_init(name, strlen(name), 0);
    zval *data = zend_hash_find(symbol_table, key_name);

    zend_string_release(key_name);
    zend_array_destroy(symbol_table);

    return data;
}

extern "C" void test_var(char * name) {
    zval *zv_ptr = get_zval_by_name(name);

    if(zv_ptr != NULL) {
        // https://www.phpinternalsbook.com/php7/zvals/basic_structure.html#access-macros
        try_again:
            switch (Z_TYPE_P(zv_ptr)) {
                case IS_NULL:
                    php_printf("NULL: null\n");
                    break;
                case IS_LONG:
                    php_printf("LONG: %ld\n", Z_LVAL_P(zv_ptr));
                    break;
                case IS_TRUE:
                    php_printf("BOOL: true\n");
                    break;
                case IS_FALSE:
                     php_printf("BOOL: false\n");
                    break;
                case IS_DOUBLE:
                    php_printf("DOUBLE: %g\n", Z_DVAL_P(zv_ptr));
                    break;
                case IS_STRING:
                   php_printf("STRING: value=\"");
                   PHPWRITE(Z_STRVAL_P(zv_ptr), Z_STRLEN_P(zv_ptr));
                   php_printf("\", length=%zd\n", Z_STRLEN_P(zv_ptr));
                   break;
                case IS_RESOURCE:
                    php_printf("RESOURCE: id=%ld\n", Z_RES_HANDLE_P(zv_ptr));
                    break;
                case IS_ARRAY:
                    php_printf("ARRAY: hashtable=%p\n", Z_ARRVAL_P(zv_ptr));
                    break;
                case IS_OBJECT:
                    php_printf("OBJECT: object=%p\n", Z_OBJ_P(zv_ptr));
                    break;
                case IS_REFERENCE:
                            php_printf("REFERENCE: ");
                            zv_ptr = Z_REFVAL_P(zv_ptr);
                            goto try_again;
                EMPTY_SWITCH_DEFAULT_CASE()
            }
        }
}

Pour l’explication, si jamais vous avez besoin d’accéder à des valeurs qui sont dans le contexte vous pouvez y accéder comme cela.

Personne ne devrait avoir besoin de ça. J’ai choisi de démontrer les possibilités de ce module par un exemple à la marge.

Le header file : export-vars-php.h

#define FFI_LIB "./php-export-vars.so"

typedef struct zval zval;
void test_var(char *name);

Le code Php : export-vars.php

#!/usr/bin/php8.1
<?php
opcache_reset();

$ffi = FFI::load(__DIR__ . '/export-vars-php.h');

$testString='jjj';
$ffi->test_var('testString');

$testInt = 10;
$ffi->test_var('testInt');

$testBool=true;
$ffi->test_var('testBool');


$testBool=false;
$ffi->test_var('testBool');

$testNull=null;
$ffi->test_var('testNull');

$testArray=[1,2,'test'];
$ffi->test_var('testArray');

Donc en exécutant notre code nous devrions obtenir ceci :

g++ -I/usr/include/php/20210902 \
    -I/usr/include/php/20210902/main \
    -I/usr/include/php/20210902/TSRM \
    -I/usr/include/php/20210902/Zend \
    -c php-export-vars.cpp && \
    gcc -shared -o php-export-vars.so  php-export-vars.o
./export-vars.php
STRING: value="jjj", length=3
LONG: 10
BOOL: true
BOOL: false
NULL: null
ARRAY: hashtable=0x7f6cd1257428

Vous remarquerez que nous sommes passés par la fonction FFI::load et que nous avons directement sorti les informations de définition dans un header file. Ça clarifie un peu le code.

Vous pouvez donc, comme étape intermédiaire, commencer à créer votre passerelle PHP sans devoir passer par la procédure de création de module PHP typique.

FFI\CData

Vous pouvez être confrontés à des fonctions prenant en paramètres différents types. Notamment des pointeurs sur des structures, des fonctions, etc.

Lors de mes tests j’ai parfois tâtonné dans leurs mises en œuvre. Voici donc pèle mêle comment créer ces données :

#!/usr/bin/php8.1
<?php
$test = FFI::new('int');
$test->cdata = 32;
var_dump($test);
echo PHP_EOL;

$value = FFI::new('char[2]');
FFI::memcpy($value, 'ab', 2);
var_dump($value);
var_dump(FFI::cast('char[2]', $value));
echo PHP_EOL;

$test = FFI::new(FFI::arrayType(FFI::type('int'), [2]));
$test[0] = 6541;
$test[1] = 8731;
var_dump($test);
echo PHP_EOL;

Pas vraiment besoin de rentrer dans le détail, les exemples sont plutôt explicites.

Voici le retour d’exécution du code :

./cdata.php 
object(FFI\CData:int32_t)#1 (1) {
  ["cdata"]=>
  int(32)
}

object(FFI\CData:char[2])#2 (2) {
  [0]=>
  string(1) "a"
  [1]=>
  string(1) "b"
}
object(FFI\CData:char[2])#3 (2) {
  [0]=>
  string(1) "a"
  [1]=>
  string(1) "b"
}

object(FFI\CData:int32_t[2])#3 (2) {
  [0]=>
  int(6541)
  [1]=>
  int(8731)
}

Vous pouvez avoir besoin de passer aux fonctions des adresses de structures. La documentation officielle est bien faite. (https://www.php.net/manual/fr/ffi.examples-basic.php)

Je vous propose l’exemple suivant :

<?php
$ffi = FFI::cdef("
    typedef unsigned int time_t;
    typedef unsigned int suseconds_t;
 
    struct timeval {
        time_t      tv_sec;
        suseconds_t tv_usec;
    };
 
    struct timezone {
        int tz_minuteswest;
        int tz_dsttime;
    };
 
    int gettimeofday(struct timeval *tv, struct timezone *tz);    
", "libc.so.6");

$tv = $ffi->new("struct timeval");
$tz = $ffi->new("struct timezone");


var_dump($ffi->gettimeofday(FFI::addr($tv), FFI::addr($tz)));


var_dump($tv->tv_sec);

var_dump($tz);
?>

Vous voyez que **int gettimeofday(struct timeval tv, struct timezone tz); prend en paramètres 2 structures. Il va donc falloir les créer via FFI::new et récupérer leurs adresses via FFI::addr. 
Tout est bien fait !

$ffi->new("struct timeval");
$ffi->new("struct timezone");

Comme nous avons passé leur adresse, nous n’avons qu’a récupérer le contenu de notre structure avec les informations renseignées. Magique !

En parlant de magie, nous allons traiter dans notre prochaine partie des fameux callbacks. Une magie dont on a du mal à se passer une fois qu’on y a goûté !

PHP FFI : CallBack – partie 3

Merci à Thomas Bourdin, Cédric Le Jallé, Stéphane Péchard pour leur aide, conseils et relecture.