PHP FFI: Pasando Parámetros – Parte 2

¿Llamando directamente a la biblioteca de PHP?

¿Qué tal si nos divertimos escribiendo un fragmento de código que es inútil? ¡Vamos! ¡Vamos! ¡Vamos!

Está bien, de acuerdo. Propongo que hagamos algo inútil utilizando PHP para llamar a una biblioteca de C que usa el Zend Engine.

De hecho, este párrafo no es tan inútil como parece. Su propósito es mostrarte un comportamiento particular 😊

PHP-FFI tiene algunas limitaciones: no puedes pasar una variable de PHP directamente para recuperar su valor dinámico desde la biblioteca externa.

Solo tienes acceso a pasar tipos de datos del lenguaje C. La forma correcta de pasar parámetros y recuperar los valores se hace a través de:

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

Lo mismo se aplica para recuperar datos.
Encontrarás más información en el sitio web oficial de PHP: https://www.php.net/manual/en/class.ffi-cdata.php.
Volveremos a esto pronto.

Sin embargo, en este ejemplo, vamos a usar Zend para recuperar el valor de una variable directamente por su nombre. Esto solo puede funcionar si la variable está dentro de su contexto. Bueno, sabe esto, esta práctica es estrictamente inútil. Pero nos permite introducir cómo usar el Zend Engine si alguna vez lo necesitáramos.

Vamos a comenzar instalando g++ y los archivos de cabecera para PHP 8.1

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

Así que aquí está nuestro código en C que nos permitirá acceder a nuestros diversos valores:

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

Para una explicación, si alguna vez necesitas acceder a valores que están en el contexto puedes acceder a ellos así.

Nadie debería necesitar esto. Elegí demostrar las capacidades de este módulo a través de un ejemplo en el límite.

El archivo de cabecera: export-vars-php.h

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

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

El código 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');

Así que al ejecutar nuestro código deberíamos obtener esto:

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

Notarás que pasamos por la función FFI::load y que directamente sacamos la información de definición en un archivo de cabecera. Esto aclara un poco el código.

Por lo tanto, como paso intermedio, puedes empezar a crear tu pasarela de PHP sin tener que pasar por el típico proceso de creación de módulos de PHP.

FFI\CData

Puede que te encuentres con funciones que toman diferentes tipos de parámetros. Notablemente punteros a estructuras, funciones, etc.

En mis pruebas, a veces me confundí en su implementación. Así que aquí están en un lío cómo crear estos datos:

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

No hay necesidad real de entrar en detalles, los ejemplos son bastante explícitos.

Aquí está la retroalimentación de la ejecución del código:

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

Puedes necesitar pasar direcciones de estructuras a las funciones. La documentación oficial está bien hecha. (https://www.php.net/manual/en/ffi.examples-basic.php)

Propongo el siguiente ejemplo:

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

Ves que **int gettimeofday(struct timeval tv, struct timezone tz); toma 2 estructuras como parámetros. Así que vas a tener que crearlas a través de FFI::new y obtener sus direcciones a través de FFI::addr.
¡Todo está bien hecho!

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

Ya que pasamos su dirección, solo tenemos que recuperar el contenido de nuestra estructura con la información completada. ¡Magia!

Hablando de magia, en nuestra próxima parte trataremos con los famosos callbacks. ¡Magia que es difícil de hacer sin una vez que lo has probado!

PHP FFI: CallBack - parte 3

Gracias a Thomas Bourdin, Cédric Le Jallé, Stéphane Péchard por su ayuda, consejos y corrección de pruebas.