PHP FFI: Callback - Part 3

The Callback

During the preparation of this article, I noticed that many resources on the internet directly use the libc for their examples. 
You can easily find fairly demonstrative and simple examples. But to do something cool and original, I thought to myself "let's find something to show with this libc too". 
After all… if it's just passing a structure as most examples do, it won't be useful. And then I came across a function in a forum where people were complaining that it's badly done, that it can't be easily used, etc. 
So I did some research, and indeed the “nftw” and “ftw” functions are somewhat impractical. They require a function as parameters.

As you can see on the man page http://manpages.ubuntu.com/manpages/bionic/en/man3/ftw.3posix.html

#include <ftw.h>

       int ftw(const char *path, int (*fn)(const char *, const struct stat *ptr, int flag), int ndirs);

So we have a function pointer as the second parameter. Alright… 
So what do we do at this point? FFI::new something like that ??
Let me explain in a few minutes how to handle this.

#!/usr/bin/php8.1
<?php
    /**
     * quelques références:
     * https://www.ibm.com/docs/en/zos/2.3.0?topic=functions-nftw-nftw64-traverse-file-tree
     * http://www.sde.cs.titech.ac.jp/~gondow/dwarf2-xml/HTML-rxref/app/gcc-3.3.2/lib/gcc-lib/sparc-sun-solaris2.8/3.3.2/include/sys/types.h.html
     * http://www.doc.ic.ac.uk/~svb/oslab/Minix/usr/include/sys/types.h
     * http://manpages.ubuntu.com/manpages/trusty/fr/man2/stat.2.html
     */
opcache_reset();

$ffi = FFI::cdef(
    "
            typedef long		blkcnt_t;	
            typedef	long		time_t;
            /* Types used in disk, inode, etc. data structures. */
            typedef short          dev_t;	   /* holds (major|minor) device pair */
            typedef char           gid_t;	   /* group id */
            typedef unsigned short ino_t; 	   /* i-node number */
            typedef unsigned short mode_t;	   /* file type and permissions bits */
            typedef char         nlink_t;	   /* number of links to a file */
            typedef unsigned long  off_t;	   /* offset within a file */
            typedef int            pid_t;	   /* process id (must be signed) */
            typedef short          uid_t;	   /* user id */
            typedef unsigned long zone_t;	   /* zone number */
            typedef unsigned long block_t;	   /* block number */
            typedef unsigned long  bit_t;	   /* bit number in a bit map */
            typedef unsigned short zone1_t;	   /* zone number for V1 file systems */
            typedef unsigned short bitchunk_t; /* collection of bits in a bitmap */
            
            typedef unsigned char   u8_t;	   /* 8 bit type */
            typedef unsigned short u16_t;	   /* 16 bit type */
            typedef unsigned long  u32_t;	   /* 32 bit type */
    

    
            typedef	int		blksize_t;	/* used for block sizes */
    
            struct timespec {
                         time_t          tv_sec;
                         long            tv_nsec;
            };
            
            /** 
             * http://manpages.ubuntu.com/manpages/trusty/fr/man2/stat.2.html
             * /usr/include/x86_64-linux-gnu/sys/types.h 
             */
            struct stat {
               dev_t     st_dev;         /* Périphérique                     */
               ino_t     st_ino;         /* Numéro d’inœud                   */
               mode_t    st_mode;        /* Protection                       */
               nlink_t   st_nlink;       /* Nombre de liens physiques        */
               uid_t     st_uid;         /* UID du propriétaire              */
               gid_t     st_gid;         /* GID du propriétaire              */
               dev_t     st_rdev;        /* Type de périphérique             */
               off_t     st_size;        /* Taille totale en octets          */
               blksize_t st_blksize;     /* Taille de bloc pour E/S          */
               blkcnt_t  st_blocks;      /* Nombre de blocs de 512 o alloués */
            
               /* Depuis Linux 2.6, le noyau permet une précision à la
                  nanoseconde pour les champs temporels suivants. Pour
                  plus de précisions avant Linux 2.6, consultez les NOTES. */
            
               struct timespec st_atim;  /* Heure dernier accès              */
               struct timespec st_mtim;  /* Heure dernière modification      */
               struct timespec st_ctim;  /* Heure dernier changement état    */
            
               #define st_atime st_atim.tv_sec      /* Rétrocompatibilité        */
               #define st_mtime st_mtim.tv_sec
               #define st_ctime st_ctim.tv_sec
            };

            typedef struct FTW{
                int base;
                int level;
            };

            typedef int (*fn)(const char *, const struct stat *, int, struct FTW *);
            int nftw(const char *path, int (*fn)(const char *, const struct stat *, int, struct FTW *),  int fd_limit, int flags);
            int ftw(const char *path, int (*fn)(const char *, const struct stat *, int, struct FTW *),  int fd_limit);
        ",
    "libc.so.6"
);

$values = [];
$fn = function ($fpath, $stat, $tflag, $ftwbuf) {
    global $values;

    /**
     * https://sites.uclouvain.be/SystInfo/usr/include/ftw.h.html
     * FTW_CONTINUE = 0,     Continue with next sibling or for FTW_D with the  first child.
     * FTW_STOP = 1,         Return from `ftw' or `nftw' with FTW_STOP as return value.
     * FTW_SKIP_SUBTREE = 2  Only meaningful for FTW_D: Don't walk through the  subtree, instead just continue with its next sibling.
     * FTW_SKIP_SIBLINGS = 3 Continue with FTW_DP callback for current directory (if FTW_DEPTH) and then its siblings.
     */
    $values[] = sprintf(
        "level: %s, Path: %-40s",
        $ftwbuf->level,
        $fpath
    );
    return 0;
};

$ffi->nftw('/var/lib', $fn, 1, 0);
print_r(array_slice($values, 0, 5));
echo PHP_EOL;


$ffi->ftw('/var/lib', $fn, 1);
print_r(array_slice($values, 0, 5));
echo PHP_EOL;

I present the code as is, with the references that allowed me to set up the example. To make it short,  I started from the man page which gives me the first information. We can start by laying out the definition with the 2 functions.


int nftw(const char *path, int (*fn)(const char *, const struct stat *, int, struct FTW *),  int fd_limit, int flags);
int ftw(const char *path, int (*fn)(const char *, const struct stat *, int, struct FTW *),  int fd_limit);

We quickly notice that we have a function pointer so we add it to our definition:


typedef int (*fn)(const char *, const struct stat *, int, struct FTW *);

Here we identify that we need to specify 2 structures : FTW but also stat

typedef struct FTW{
                int base;
                int level;
};
            struct stat {
               dev_t     st_dev;         /* Périphérique                     */
               ino_t     st_ino;         /* Numéro d’inœud                   */
               mode_t    st_mode;        /* Protection                       */
               nlink_t   st_nlink;       /* Nombre de liens physiques        */
               uid_t     st_uid;         /* UID du propriétaire              */
               gid_t     st_gid;         /* GID du propriétaire              */
               dev_t     st_rdev;        /* Type de périphérique             */
               off_t     st_size;        /* Taille totale en octets          */
               blksize_t st_blksize;     /* Taille de bloc pour E/S          */
               blkcnt_t  st_blocks;      /* Nombre de blocs de 512 o alloués */
            
               /* Depuis Linux 2.6, le noyau permet une précision à la
                  nanoseconde pour les champs temporels suivants. Pour
                  plus de précisions avant Linux 2.6, consultez les NOTES. */
            
               struct timespec st_atim;  /* Heure dernier accès              */
               struct timespec st_mtim;  /* Heure dernière modification      */
               struct timespec st_ctim;  /* Heure dernier changement état    */
            
               #define st_atime st_atim.tv_sec      /* Rétrocompatibilité        */
               #define st_mtime st_mtim.tv_sec
               #define st_ctime st_ctim.tv_sec
            };

            typedef struct FTW{
                int base;
                int level;
            };

A quick search on the internet and we easily find where to copy and paste the information :

http://manpages.ubuntu.com/manpages/trusty/fr/man2/stat.2.html but especially /usr/include/x86_64-linux-gnu/sys/types.h.

Convenient, I already have the information on hand 🙂 We continue unravelling the thread and add all the types. It was a bit difficult to find them…

http://www.sde.cs.titech.ac.jp/~gondow/dwarf2-xml/HTML-rxref/app/gcc-3.3.2/lib/gcc-lib/sparc-sun-solaris2.8/3.3.2/include/sys/types.h.html 
http://www.doc.ic.ac.uk/~svb/oslab/Minix/usr/include/sys/types.h

There, our definition is done! We finally get to the amazing functionality I'm trying to demonstrate here: The callback!
Yes, you heard right. 
Since we have no limitations, we're going to pass a PHP anonymous function directly to our ftw function... wow wow WOOOOW  WOOOOW!!!

I'm getting excited, but it's actually super cool, look at the code :

$values = [];
$fn = function ($fpath, $stat, $tflag, $ftwbuf) {
    global $values;

    /**
     * https://sites.uclouvain.be/SystInfo/usr/include/ftw.h.html
     * FTW_CONTINUE = 0,     Continue with next sibling or for FTW_D with the  first child.
     * FTW_STOP = 1,         Return from `ftw' or `nftw' with FTW_STOP as return value.
     * FTW_SKIP_SUBTREE = 2  Only meaningful for FTW_D: Don't walk through the  subtree, instead just continue with its next sibling.
     * FTW_SKIP_SIBLINGS = 3 Continue with FTW_DP callback for current directory (if FTW_DEPTH) and then its siblings.
     */
    $values[] = sprintf(
        "level: %s, Path: %-40s",
        $ftwbuf->level,
        $fpath
    );
    return 0;
};

$ffi->nftw('/var/lib', $fn, 1, 0);

Okay, well, I didn't think I was going to rewrite a global in my life, the last time was maybe 15 years ago, and even then… But for the purposes of the demo, whatever , YOLO (up to you to come up with another acronym…). 
As you can see, there's no need to pass an FFI::addr, the anonymous function is sufficient on its own.

The result :

./nftw.php 
Array
(
    [0] => level: 0, Path: /var/lib                                
    [1] => level: 1, Path: /var/lib/php                            
    [2] => level: 2, Path: /var/lib/php/sessions                   
    [3] => level: 2, Path: /var/lib/php/modules                    
    [4] => level: 3, Path: /var/lib/php/modules/7.4                
)

Array
(
    [0] => level: 0, Path: /var/lib                                
    [1] => level: 1, Path: /var/lib/php                            
    [2] => level: 2, Path: /var/lib/php/sessions                   
    [3] => level: 2, Path: /var/lib/php/modules                    
    [4] => level: 3, Path: /var/lib/php/modules/7.4                
)

The documentation does warn us about callbacks. They shouldn't be overused.

In our next part of the file, we will address how to play the card of interoperability with languages designed for mobile. KMP (Kotlin Multiplatform).

PHP FFI: using a Kotlin Multiplatform library - part 4

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