PHP 7.4 et FFI, ce qu’il faut retenir

(🇬🇧 Read the english version here)

PHP Foreign Function Interface, ou PHP FFI pour les intimes, ou FFI pour les fans, est une extension PHP qui permet d’inclure facilement des bibliothèques externes au sein de PHP. Autrement dit, il est possible d’utiliser directement des librairies partagées écrite en C, Go, Rust, etc. depuis PHP sans avoir besoin de créer d’extension PHP en C. C’est un mécanisme qui existe depuis très longtemps dans d’autre langage comme Python, ce qui a fait entre autre une de ses forces.

Génération d’UUID

Prenons un exemple pour se chauffer : la génération d’UUID.

En PHP il existe plusieurs méthodes pour générer un UUID. La meilleure méthode est de passer via la PECL UUID. Vous pouvez retrouver son code sur github. Cette extension PHP se charge d’associer des functions qui seront utilisable par un développeur PHP à des appels sur la libuuid. Pour fonctionner, il faut obligatoirement installer libuuid sur son système (ce qui est déjà installé la plupart du temps), ainsi que la PECL.

Nous pouvons décrire le fonctionnement d’un appel à une librairie externe depuis PHP comme cela :

   +---------------------+
   |    Votre code PHP   |
   +---+-------------^---+
       v             ^
   +---v-------------+---+
   |    Le moteur PHP    |
   +---+-------------^---+
       v             ^
   +---v-------------+---+
   |     L'ext UUID      |
   +---+-------------^---+
       v             ^
   +---v-------------+---+
   |     La lib UUID     |
   +---------------------+

La promesse de FFI, c’est de remplacer la couche « extension UUID » par du code PHP.

Avant de parler de la couche extension PHP ou de FFI, il faut expliquer ce qu’est une librairie. Une librairie est habituellement écrite en C. Mais elle peut aussi être écrite dans d’autres langages qui sont capable de se compiler en une librairie partagée : C++, Rust, Go, etc. Sous unix ou linux, la librairie sera compilée en un fichier .so et sous Windows en un fichier .dll. Il est aussi possible d’inclure directement une librairie dans un programme, mais ce chapitre ne nous intéresse pas pour cet article.

Dans le code source de la librairie, il existe des fichiers .h qui contiennent ce que la librairie est capable de faire. Voici un extrait du fichier uuid.h :

# ...
# Quelques déclarations de constantes :
#define UUID_VARIANT_NCS    0
#define UUID_VARIANT_DCE    1
#define UUID_VARIANT_MICROSOFT  2
#define UUID_VARIANT_OTHER  3

# Quelques declarations de fonctions :
void uuid_generate(uuid_t out);
int uuid_compare(const uuid_t uu1, const uuid_t uu2);
# ...

Un fichier .h est quelque chose que nous pourrions comparer avec une interface en PHP : il regroupe les constantes et les fonctions exposées par la librairie.

Création de la couche FFI/UUID

Pour fonctionner, FFI a besoin des signatures que nous voulons utiliser de la librairie sous jacente. Nous allons copier le fichier .h dans notre projet et au besoin, nous nettoierons et adapterons le fichier. Nous pourrions, par exemple, supprimer les fonctions qui ne nous seront pas utiles. Voici à quoi ressemble le fichier final pour notre librairie :

#define FFI_LIB "libuuid.so.1"

typedef unsigned char uuid_t[16];

extern void uuid_generate_time(uuid_t out); // v1
extern void uuid_generate_md5(uuid_t out, const uuid_t ns, const char *name, size_t len); // v3
extern void uuid_generate_random(uuid_t out); // v4
extern void uuid_generate_sha1(uuid_t out, const uuid_t ns, const char *name, size_t len); // v5

Ce travail est la partie la plus importante et la plus fastidieuse. Un fois effectué, on charge ce fichier dans PHP/FFI :

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

Et Voilà ! Nous sommes maintenant en mesure d’utiliser libuuid directement depuis PHP. Cependant, les fonctions de libuuid attendent des paramètres en entrée d’un certain type. Comme vous avez pu le voir dans la signature des fonctions, celles-ci ne retournent pas un UUID, mais vont modifier la première valeur passée par référence. Nous devons créer cette valeur en amont avant de pouvoir utiliser la fonction :

$output = $ffi->new('uuid_t');

$output est une instance de type FFI\CData. Suivant le type interne de CDATA, nous pourrons accéder aux différents valeurs décrites dans la documentation.

Enfin, nous pouvons appeler notre fonction. uuid_generate_random correspond au nom exposé par la librairie dans le fichier .h :

$ffi->uuid_generate_random($output);

Le contenu de $output va être mis à jour avec un tableau contenant les valeurs (decimal) de notre UUID. Il ne reste plus qu’à convertir ce tableau en une string contenant les valeurs (hexadécimal) :

foreach ($output as $values[]);

$uuid = sprintf('%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x', ...$values);

Simple, non ? Si vous ne voulez pas vous embêter à refaire tout ce code pour supporter la libuuid, nous avons open-sourcé un paquet pour vous : https://github.com/jolicode/ffi-uuid🍾

Quelques considérations

Simplicité

Le binding d’une librairie externe est relativement simple. Le plus compliqué est la création d’un fichier .h minimal, ainsi qu’un mapping des types PHP vers les types de la librairie et vice-versa.

Performance

Il est aussi intéressant de regarder les performances de notre implémentation. Dans le dépôt de code vous pourrez retrouver un script de benchmark. Voilà les résultats lorsque nous comparons la PECL à notre code :

FFI:
 * [v1] 1.254s
 * [v4] 5.301s
PECL:
 * [v1] 0.626s
 * [v4] 4.583s

Nous pouvons voir que la PECL est deux fois plus rapide que notre implémentation pour la version 1 mais seulement 15% plus rapide pour la version 4. Cela peut s’expliquer assez facilement : un UUID v4 est composé exclusivement de nombres aléatoires, alors que la version 1 contient des blocs de nombres invariants. La récupération de nombres aléatoires est quelque chose d’un peu lent, c’est pourquoi un UUID v4 est beaucoup plus lent à générer qu’un UUID v1. Le différence entre les deux implémentations est moins visible sur la v4 car le temps de génération est essentiellement passé dans la libuuid.

Que pouvons nous en conclure ?

FFI est encore très jeune, nous pouvons nous attendre à des améliorations de performance. Cependant, nous pouvons déjà tirer quelques conclusions :

  • Si une extension native à PHP existe et que vous pouvez l’installer : utilisez la ;
  • Si l’extension n’existe pas, alors FFI est une très bonne alternative ;
  • Si vous avez un bottleneck dans votre application, il peut être intéressant de porter cette partie de code en C, Rust, ou Go, et de binder ce code avec FFI. FFI va devenir très intéressant lorsque l’on doit faire beaucoup de calcul CPU (gestion de DOM, de gros tableau, de calcul mathématique).

Les extensions PHP vont elles être remplacées par FFI ?

Il est beaucoup trop tôt pour répondre à cette question qui a le mérite d’être posée. Cependant, des extensions comme PDO font beaucoup plus qu’un simple binding à une lib sous jacente. Je suis très confiant sur le fait que ces extensions ne seront pas remplacées par FFI.

Cependant d’autres extensions plus simple pourront sûrement être remplacées. C’est le cas de php-redis, amqp, uuid, etc. D’ailleurs Remi Collet à déjà commencé à jouer avec FFI pour remplacer l’extension redis.

FFI ouvre aussi les portes au remplacement de certaines lib écrites en pur PHP alors que des lib bas niveau existent. C’est le cas de gitlib qui pourrait être remplacée par un portage FFI de libgit2

Et enfin, dans certains cas, il n’existe ni extension C, ni implémentation PHP. Si vous avez déjà voulu tester TensorFlow via PHP, vous avez pu vous rendre compte que … c’est compliqué. Dmitry Stogov, l’un des plus important contributeur à PHP – mais aussi l’auteur de PHP/FFI, a créé un POC pour binder tensor flow à PHP.

Quel langage choisir pour binder une lib en PHP ?

Tous les langages qui peuvent compiler une librairie partagée (.so) ne sont pas forcément adaptés à être binder en FFI. Il est préférable d’utiliser des langages sans runtime (C / C++ / Rust / …) car inclure un runtime peut avoir des conséquences non souhaitées selon ce que fait la librairie. En Go par exemple, le runtime va être capable de gérer un garbage collector ou encore une event loop (pour les goroutines). Si jamais votre programme PHP gère aussi une event loop, il peut arriver que certains événements ne soient jamais dispatchés à votre librairie partagée et donc que les goroutines ne soient jamais exécutées.

Comment binder une lib Rust en PHP ?

J’ai voulu tester s’il était plus rapide d’exécuter des calculs un peu compliqués dans un autre langage. C’est assez fréquent d’avoir à extraire des informations d’une page web, que ce soit pour des tests fonctionnels, ou pour faire un crawler.

Joel a fait une petite librairie qui permet d’extraire le premier élément HTML d’un document qui matche une expression CSS. Le code est très simple, à tel point que la conversion de type C vers Rust représente plus d’un tiers du projet.

Le binding PHP quand à lui ressemble fortement à ce que nous avons vu précédemment :

$ffi = FFI::cdef(<<<EOH
const char *cssfilter(const char *html, const char *filter);
EOH, __DIR__.'/../target/release/libcssfilter.so');

Et son utilisation est même encore plus simple :

$value = $ffi->cssfilter($html, $selector);

Les performances sont assez impressionnantes et encourageantes :

FFI:
 Duration: 1.731s
symfony/crawler:
 Duration: 2.321s

Nous pouvons facilement en conclure que si les calculs sont compliqués, il peut être très intéressant de porter une partie du code dans un autre langage pour améliorer les performances de notre application.

Conclusion

FFI n’est pas encore sorti officiellement, mais j’en suis déjà fan 🤩. FFI va nous permettre de tester rapidement des librairies alors que les bindings officiels ne sont pas encore disponibles. Il va aussi nous permettre de remplacer certaines parties du code qui sont trop lentes à nos yeux par une implémentation en Rust ou autre.

Bref, l’essayer, c’est l’adopter.

Nos formations sur le sujet

  • Logo Symfony avancée

    Symfony avancée

    Décou­vrez les fonc­tion­na­li­tés et concepts avan­cés de Symfo­ny

blog comments powered by Disqus