Accéder au contenu principal

7min.

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.

Section intitulée generation-d-uuidGé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.

Section intitulée creation-de-la-couche-ffi-uuidCré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🍾

Section intitulée quelques-considerationsQuelques considérations

Section intitulée simpliciteSimplicité

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.

Section intitulée performancePerformance

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.

Section intitulée que-pouvons-nous-en-conclureQue 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).

Section intitulée les-extensions-php-vont-elles-etre-remplacees-par-ffiLes 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.

Section intitulée quel-langage-choisir-pour-binder-une-lib-en-phpQuel 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.

Section intitulée comment-binder-une-lib-rust-en-phpComment 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.

Section intitulée conclusionConclusion

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.

Commentaires et discussions

Nos articles sur le mĂŞme sujet

Nos formations sur ce sujet

Notre expertise est aussi disponible sous forme de formations professionnelles !

Voir toutes nos formations

Ces clients ont profité de notre expertise