PHP FFI: Passing Parameters – Part 2

Calling the PHP library directly?

What if we had fun writing a piece of code that is useless? Come on!!! Come on!! Come on!!!

Alright OK. I propose we do something useless by using PHP to call a C library that uses the Zend Engine.

Actually, this paragraph is not as useless as it seems. Its purpose is to show you a particular behavior 😊

PHP-FFI has a few limitations: you can't pass a PHP variable directly to retrieve its dynamic value from the external library.

You only have access to passing C language data types. The real proper way to pass parameters and retrieve the values is through:

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

The same goes for retrieving data.
You'll find more info on the official PHP website: https://www.php.net/manual/en/class.ffi-cdata.php.
We'll get back to this soon.

However, in this example, we're going to use Zend to retrieve the value of a variable directly by its name. This can only work if the variable is within its context. Well, know this, this practice is strictly useless. But it allows us to introduce how to use the Zend Engine if we ever needed it.

We're going to start by installing g++ and the header files for PHP 8.1

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

So here is our C code that will allow us to access our various values:

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

For an explanation, if you ever need to access values that are in the context you can access them like this.

No one should need this. I chose to demonstrate the capabilities of this module through an example on the fringe.

The header file: export-vars-php.h

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

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

The PHP code: 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');

So by running our code we should get this:

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

You'll notice that we went through the FFI::load function and that we directly output the definition information into a header file. It clarifies the code a bit.

You can therefore, as an intermediate step, start to create your PHP gateway without having to go through the typical PHP module creation process.

FFI\CData

You may face functions taking different types of parameters. Notably pointers to structures, functions, etc.

In my tests, I sometimes fumbled in their implementation. So here are in a jumble how to create these data:

#!/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 real need to get into the details, the examples are quite explicit.

Here is the code execution feedback:

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

You may need to pass addresses of structures to the functions. The official documentation is well done. (https://www.php.net/manual/en/ffi.examples-basic.php)

I propose the following example:

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

You see that **int gettimeofday(struct timeval tv, struct timezone tz); takes 2 structures as parameters. So you're going to have to create them through FFI::new and get their addresses through FFI::addr.
Everything is well made!

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

Since we passed their address, we only have to retrieve the content of our structure with the information filled in. Magic!

Speaking of magic, in our next part we will deal with the famous callbacks. Magic that's hard to do without once you've tasted it!

PHP FFI: CallBack - part 3

Thank you to Thomas Bourdin, Cédric Le Jallé, Stéphane Péchard for their help, advice, and proofreading.