PHP FFI: Passaggio di Parametri – Parte 2

Chiamare direttamente la libreria PHP?

Che ne direste di divertirci a scrivere un frammento di codice che è inutile? Dài!!! Dài!! Dài!!!

OK va bene. Propongo di fare qualcosa di inutile utilizzando PHP per chiamare una libreria C che usa il Zend Engine.

In realtà, questo paragrafo non è inutile come sembra. Il suo scopo è quello di mostrarvi un comportamento particolare 😊

PHP-FFI ha alcune limitazioni: non puoi passare direttamente una variabile PHP per recuperarne il valore dinamico dalla libreria esterna.

Hai accesso solo al passaggio di tipi di dati del linguaggio C. Il modo corretto reale per passare i parametri e recuperare i valori è attraverso:

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

Lo stesso vale per il recupero dei dati.
Troverete maggiori informazioni sul sito ufficiale di PHP: https://www.php.net/manual/it/class.ffi-cdata.php.
Ne parleremo più avanti.

Tuttavia, in questo esempio, utilizzeremo Zend per recuperare il valore di una variabile direttamente per nome. Questo può funzionare solo se la variabile si trova nel suo contesto. Bene, sappiate che questa pratica è strettamente inutile. Ma ci permette di introdurre come utilizzare il Zend Engine se ne avessimo bisogno.

Iniziamo installando g++ e i file di intestazione per PHP 8.1

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

Quindi ecco il nostro codice C che ci permetterà di accedere ai nostri vari valori:

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()
            }
        }
}

Per spiegazione, se hai bisogno di accedere a valori che sono nel contesto puoi accedervi in questo modo.

Nessuno dovrebbe averne bisogno. Ho scelto di dimostrare le capacità di questo modulo attraverso un esempio ai margini.

Il file di intestazione: export-vars-php.h

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

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

Il codice 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');

Quindi, eseguendo il nostro codice dovremmo ottenere questo:

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

Noterete che abbiamo utilizzato la funzione FFI::load e che abbiamo direttamente inserito le informazioni di definizione in un file di intestazione. Ciò chiarisce un po' il codice.

Quindi, come passaggio intermedio, potete iniziare a creare il vostro gateway PHP senza dover passare attraverso il tipico processo di creazione di un modulo PHP.

FFI\CData

Potrete incontrare funzioni che richiedono tipi di parametri diversi. Notabilmente puntatori a strutture, funzioni, ecc.

Nelle mie prove, a volte ho avuto problemi nella loro implementazione. Quindi ecco in un miscuglio come creare questi dati:

#!/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;

Non c'è realmente bisogno di entrare nei dettagli, gli esempi sono abbastanza espliciti.

Ecco il feedback dell'esecuzione del codice:

./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)
}

Potresti aver bisogno di passare gli indirizzi delle strutture alle funzioni. La documentazione ufficiale è ben fatta. (https://www.php.net/manual/it/ffi.examples-basic.php)

Propongo il seguente esempio:

<?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);
?>

Vedete che **int gettimeofday(struct timeval tv, struct timezone tz); prende come parametri 2 strutture. Quindi dovrai crearle tramite FFI::new e ottenere i loro indirizzi tramite FFI::addr.
Tutto è ben fatto!

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

Dato che abbiamo passato il loro indirizzo, non ci resterà che recuperare il contenuto della nostra struttura con le informazioni riempite. Magia!

Parlando di magia, nella nostra prossima parte affronteremo i famosi callback. Una magia difficile da fare a meno una volta che l'hai assaporata!

PHP FFI: CallBack - parte 3

Grazie a Thomas Bourdin, Cédric Le Jallé, Stéphane Péchard per il loro aiuto, consigli e correzione di bozze.