Contact salesFree trial
Blog

Intégration des bibliothèques C avec PHP FFI

PHPCLIconfigurationUpsunify
26 février 2020
Partager

Aujourd'hui, nous allons insérer cette bibliothèque C dans PHP et la faire fonctionner sur Upsun.

Comprendre l'interface de fonction étrangère de PHP

FFI, ou Foreign Function Interface, est une fonctionnalité de nombreux langages qui permet à ce langage d'appeler du code écrit dans un autre langage. PHP a obtenu cette fonctionnalité dans la version PHP 7.4, bien que, comme on peut s'y attendre, il y ait quelques problèmes sur la route.

L'un des problèmes est que PHP est, par nature, accessible par des systèmes distants. Cela crée un risque de sécuriténaturel . Avec FFI, si vous pouvez exploiter n'importe quelle faille dans une application, alors vous pouvez potentiellement obtenir une faille d'exécution de code à distance pour le code au niveau du système. Sur une échelle de sécurité de 1 à 10, c'est le niveau le plus élevé, donc par défaut, PHP ne supporte même pas cela. FFI n'est activé par défaut qu'à partir du CLI ou dans le code préchargé.

Il y a cependant une mise en garde. Le préchargement (également nouveau en PHP 7.4, plus d'informations à ce sujet dans un instant) s'appuie sur l'opcache. Il en est de même pour FFI. L'opcache, cependant, est désactivé par défaut puisque dans le CLI, il n'y a aucun endroit où les opcodes mis en cache peuvent être conservés d'une exécution à l'autre. Pour utiliser FFI avec la CLI, nous allons donc devoir activer manuellement l'opcache.

FFI dans l'interface de programmation

Commençons par un peu de code pour utiliser la bibliothèque de points que nous avons créée plus tôt. Tout d'abord, nous aurons besoin d'une structure en PHP pour imiter la structure de points. (Ce n'est pas nécessaire, mais le fait de refléter les objets de valeur des deux côtés rend l'API plus facile à suivre).

class Point { public int $x ; public int $y ; public function __construct(int $x, int $y) { $this->x = $x ; $this->y = $y ; } }

L'étape suivante consiste à indiquer à PHP l'existence de la bibliothèque. Il y a plusieurs façons de le faire, mais nous allons commencer par cdef():

$ffi = FFI::cdef(file_get_contents('points.h'), __DIR__ . '/points.so') ;

FFI::cdef() n'est pas une leçon d'alphabet qui a mal tourné ; elle signifie "définition C" et prend deux paramètres : une chaîne de caractères qui définit les structures C à exposer et le chemin vers le fichier .so dans lequel elles peuvent être trouvées.

Dans ce cas, il est plus facile de lire le fichier .h pour la définition. Cependant, la FFI de PHP n'utilise pas d'analyseur d'en-tête C standard. Il a son propre analyseur, qui à ce jour est... plutôt faible. Il ne supporte pas beaucoup de syntaxes qui sont tout à fait légales dans le code C moderne, et qui sont en fait assez courantes dans la plupart des programmes C actuels. Alors que les bibliothèques personnalisées comme celle-ci peuvent réutiliser leurs en-têtes, la plupart des bibliothèques C de production ont des en-têtes trop complexes pour être gérés par le FFI de PHP. Cela signifie que vous devrez réimplémenter les vôtres.

Le couplage entre un fichier d'en-tête et une bibliothèque est beaucoup plus faible que, disons, une interface et une classe PHP. Le fichier d'en-tête dit simplement : "Il existe, quelque part, une fonction nommée distance qui prend 2 points". Le fichier .so lui-même publie "BTW, j'ai une fonction nommée distance qui prend 2 points". Tant que ces éléments sont alignés au moment du chargement, tout se passe bien.

Cela signifie aussi que si nous ne voulons exposer qu'une petite partie d'une grande bibliothèque, nous pouvons écrire un fichier d'en-tête qui ne déclare que les fonctions et les structures que nous voulons exposer à PHP, laissant le reste inaccessible. Cela peut être utile pour les grandes bibliothèques.

Nous ne nous étendrons pas sur le sujet ici, mais Anthony Ferrara a écrit un outil appelé FFIMe qui enveloppe l'API FFI pour la rendre plus facile à utiliser et qui gère également le prétraitement des en-têtes C dans le sous-ensemble du langage supporté par PHP. Ce n'est pas parfait, mais cela vaut la peine de l'étudier pour une utilisation sérieuse de FFI.
Nous pouvons maintenant utiliser la variable $ffi pour accéder aux parties du fichier .so qui sont exposées.

// inline.php // ...

$p1 = nouveau Point(3, 4) ; $p2 = nouveau Point(7, 9) ; $cp1 = $ffi->new('struct point') ; $cp2 = $ffi->new('struct point') ; $cp1->x = $p1->x ; $cp1->y = $p1->y ; $cp2->x = $p2->x ; $cp2->y = $p2->y ; $d = $ffi->distance($cp1, $cp2) ; print "Distance is $d\n" ;

Tout d'abord, nous créons deux objets Point. Ensuite, nous créons deux variables qui sont des ponts vers la structure point du fichier d'en-tête. Ces variables sont de type FFI\CData et agissent comme un adaptateur entre le langage PHP et le langage C. Comme ce sont des objets, nous pouvons les utiliser en tant qu'objets et elles gèrent la traduction vers le C, donc copier nos valeurs d'objets Point PHP vers elles est un processus simple et ennuyeux.

Enfin, nous pouvons appeler la méthode distance sur l'objet $ffi, qui est un adaptateur pour la fonction C. Les données sont transférées à la fonction C. Les données sont transmises à la bibliothèque C qui produit un double (alias un float) et le renvoie. L'exécution de ce code imprime consciencieusement Distance is 6.4031242374328.

Ou presque. Rappelons que nous avons dit plus haut que FFI nécessite un opcache fonctionnel, ce que le CLI n'a pas par défaut. Nous pouvons soit l'activer dans php.ini, soit sur la ligne de commande elle-même. Cette dernière solution est la plus simple :

$ php -d opcache.enable_cli=true inline.php La distance est de 6.4031242374328

Nous obtenons maintenant le résultat que nous souhaitons.

Mise en place à l'avance

Il y a cependant un problème. L'instanciation de la bibliothèque FFI, l'analyse du fichier d'en-tête et la configuration des adaptateurs appropriés en arrière-plan prennent un temps non négligeable, surtout si la bibliothèque C est plus importante que ce simple exemple. Dans l'interface de programmation, il n'y a pas de bon moyen d'éviter ce coût à chaque requête, mais une fois que nous connectons ce code à une requête web, nous voulons éviter ce surcoût si possible.

Heureusement, FFI offre une alternative pour passer à une bibliothèque C qui n'a pas de surcoût d'exécution et qui utilise le préchargement. Le préchargement est une autre nouvelle fonctionnalité de PHP 7.4 qui vous permet de charger les définitions de classes et de fonctions dans la mémoire de PHP-FPM une fois au démarrage du serveur et de ne plus jamais les recharger. Nous avons déjà écrit sur le préchargement, et c'est un ajout vraiment intéressant.

Vous pouvez également initialiser une bibliothèque C via le préchargement et la garder en mémoire, évitant ainsi le coût d'initialisation dans les requêtes futures. Par défaut, c'est la seule façon de l'utiliser dans une requête web, pour les raisons de sécurité mentionnées plus haut. Il est possible d'activer FFI::cdef() dans les requêtes web pour le développement, mais ne le faites jamais en production.

Nous allons d'abord montrer le préchargement en ligne de commande. Puisque nous utilisons le préchargement de toute façon, nous allons déplacer la définition de la classe Points dans un autre fichier nommé classes.php, comme si nous étions en 2006. (C'est rétro.) Ensuite, nous devons apporter quelques modifications supplémentaires au fichier d'en-tête en ajoutant ces deux lignes au début :

#define FFI_SCOPE "POINTS" #define FFI_LIB "./points.so"

FFI_SCOPE spécifie un identifiant unique par lequel PHP FFI reconnaîtra cette bibliothèque. FFI_LIB spécifie le fichier de la bibliothèque auquel il se réfère. Notez que le chemin est évalué relativement au chemin de la bibliothèque système, qui n'inclut pas le répertoire courant par défaut. Cela signifie que le ./ est nécessaire et que s'il n'est pas présent, le chargement échouera.

Autre mise en garde importante concernant le fichier d'en-tête : À ce jour, un bogue dans PHP l' empêche d'analyser le fichier d'en-tête s'il y a des lignes de commentaires avant une ligne #define. Toutes les déclarations #define doivent être précédées de commentaires. Nous avons signalé ce bogue à PHP (voir le lien précédent) et nous espérons qu'il sera résolu, ou au moins documenté, dans une prochaine version.

Nous pouvons maintenant mettre en place un script de préchargement. Dans ce cas, il s'agit d'un fichier très simple :

<?php // preloader.php declare(strict_types=1) ; FFI::load(__DIR__ . "/points.h") ; opcache_compile_file(__DIR__ . "/classes.php") ;

L'appel à opcache_compile_file() indique au préchargeur de charger tout ce qui se trouve dans classes.php dans la mémoire partagée de PHP. L'autre appel, FFI::load(), demande à PHP de charger le fichier d'en-tête, puis de créer et de sauvegarder l'adaptateur FFI pour cette définition. Le fichier de bibliothèque à utiliser et l'identifiant de cette bibliothèque sont spécifiés dans le fichier d'en-tête.

Enfin, nous pouvons mettre en place un autre script de test pour voir la version préchargée en action ; il peut être presque identique à la version en ligne, avec juste une modification du code de configuration FFI :

<?php // préchargement.php $ffi = \FFI: :scope("POINTS") ; $p1 = nouveau Point(3, 4) ; $p2 = nouveau Point(7, 9) ; $cp1 = $ffi->new('struct point') ; $cp2 = $ffi->new('struct point') ; $cp1->x = $p1->x ; $cp1->y = $p1->y ; $cp2->x = $p2->x ; $cp2->y = $p2->y ; $d = $ffi->distance($cp1, $cp2) ; print "Distance is $d\n" ;

L'adaptateur $ffi est maintenant créé en utilisant FFI::scope() et en spécifiant la bibliothèque déjà chargée(POINTS) que nous voulons utiliser. Comme la bibliothèque est déjà chargée en mémoire lors de l'étape de préchargement, cet appel est beaucoup moins coûteux. Le reste du code est identique.

Notez également que, puisque nous l'avons préchargée, il n'est pas nécessaire d'exiger le fichier classes.php. Il est déjà en mémoire et utilisable.

Pour exécuter ce script à partir de la ligne de commande, nous devons spécifier le script de préchargement à utiliser, soit en ligne, soit via php.ini:

$php -d opcache.preload="preloader.php" -d opcache.enable_cli=true preload.php La distance est de 6.4031242374328

Et voilà.

Améliorer l'API

L'API FFI peut parfois sembler peu naturelle ; en fait, elle l'est souvent, en particulier lorsque vous commencez à utiliser ses fonctionnalités plus avancées que nous n'allons pas aborder ici. Heureusement, en tant que programmeurs, nous disposons d'une solution standard pour les API encombrantes : "Hé, regardez, une abstraction !"

Nous allons apporter deux modifications principales à notre base de code. Tout d'abord, nous allons permettre aux objets Point de se convertir en variables FFI :

class Point { public int $x ; public int $y ; public function __construct(int $x, int $y) { $this->x = $x ; $this->y = $y ; } public function toStruct($ffi) { $cp = $ffi->new('struct point') ; $cp->x = $this->x ; $cp->y = $this->y ; return $cp ; } }

Deuxièmement, nous allons envelopper la logique FFI dans un autre objet. Il y a une douzaine de façons de le faire en fonction du contexte de ce que vous faites, mais nous allons opter pour la simplicité pour l'instant, juste pour marquer un point (sans jeu de mots) :

class PointApi { private static $ffi = null ; public function __construct() { static::$ffi ? ?= \FFI::scope("POINTS") ; } public function distance(Point $p1, Point $p2) : float { $cp1 = $p1->toStruct(static::$ffi) ; $cp2 = $p2->toStruct(static::$ffi) ; return static::$ffi->distance($cp1, $cp2) ; } }

Une portée FFI donnée peut être référencée plusieurs fois en toute sécurité, donc plutôt que d'essayer d'en faire un singleton global et de l'injecter, nous allons simplement la stocker dans une variable statique privée.

Maintenant, nous pouvons simplifier notre script d'exemple en le résumant à ceci :

<?php declare(strict_types=1) ; $p1 = nouveau Point(3, 4) ; $p2 = nouveau Point(7, 9) ; $api = nouveau PointApi() ; $d2 = $api->distance($p1, $p2) ; print "Distance is $d2\n" ;

Pas de FFI en vue, mais c'est ce qui se passe dans les coulisses.

Compiler la bibliothèque C pour la FFI PHP

La question suivante, bien sûr, est de savoir comment faire fonctionner tout ce code pour les requêtes web. C'est assez simple sur Upsun, mais les éléments mobiles sont essentiellement les mêmes sur n'importe quel serveur.

Nous commencerons avec notre dernier code dans un dépôt de projet et supposerons que les routes.yaml et un services.yaml vide sont déjà configurés. Ensuite, nous ajouterons un répertoire web avec un simple script web qui est juste le script preload.php de tout à l'heure, avec notre API nettoyée.

La partie intéressante se trouve dans le fichier .platform.app.yaml, où il y a quelques parties mobiles. Nous allons compiler la bibliothèque partagée à chaque compilation afin d'avoir toujours la dernière version. Heureusement, gcc est déjà disponible sur tous les environnements Upsun et c'est donc trivial à faire. Voici l'exemple complet, avec les commentaires pertinents :

name : app type : php:7.4 # FFI est une extension. Elle doit être explicitement activée. runtime : extensions : - ffi # Définir une variable dans l'espace de noms `php` en fait un paramètre ini. Ce bloc # dit à PHP-FPM de lancer `preloader.php` au démarrage. C'est le même fichier que nous avons vu précédemment. variables : php : opcache.preload : "preloader.php" hooks : build : # En utilisant le Makefile que nous avons défini dans la partie 1, compilez le fichier `points.so`. set -e make points.so web : locations : "/" : root : "web" passthru : "/index.php" disk : 128


Maintenant, à chaque poussée git, la bibliothèque points sera recompilée dans un nouveau fichier .so. Au moment du déploiement, PHP-FPM démarrera, chargera l'extension FFI, et lancera le script unique preloader.php pour initialiser notre portée FFI POINTS (et tout ce que nous voulons faire d'autre). La configuration web est aussi simple que possible ; chaque requête va à notre fichier index.php de base, qui est le même code que celui que nous avons déjà vu.

Si nous voulions nous assurer que la bibliothèque FFI fonctionne à partir de la ligne de commande, par exemple pour une tâche cron, la façon la plus simple de le faire est de s'assurer que la commande cron inclut les directives opcache.preload et opcache.enable_cli comme indiqué ci-dessus. Ces deux directives sont nécessaires.

Mais est-ce judicieux ? C'est l'iFFI

Et voilà ! Peu de code, mais beaucoup de concepts. Le jeu en vaut-il la chandelle ?

Comme c'est généralement le cas, la réponse est "ça dépend". La plupart du temps ? Non, cela ne vaut pas la peine. Pour un cas aussi trivial que celui-ci, c'est contre-productif.

D'une part, le PHP moderne est déjà très rapide. Les bénéfices en termes de performances du transfert du code PHP vers le C sont marginaux la plupart du temps ; l'élimination d'un ou deux appels à la base de données permet d'obtenir une bien meilleure vitesse avec beaucoup moins d'efforts.

D'autre part, FFI a ses propres frais généraux. Chaque fois que vous appelez à travers la frontière PHP/C, il y a une surcharge de traduction convertissant les structures de données d'un style à l'autre. Le simple fait d'accéder à une variable via FFI peut être deux fois plus lent que si la variable était en PHP. Pour un code trivial comme cet exemple, c'est presque certainement plus lent que d'implémenter directement distance() en PHP.

Alors, quand est-il approprié de déplacer la logique de PHP vers le C en utilisant FFI ? Je vois deux cas d'utilisation principaux :

  • Lorsque vous avez une quantité importante de traitements lourds à effectuer. Souvent, cela sera fait hors ligne dans un script de ligne de commande ou un processus de travail, mais cela peut être fait dans une requête web si cela doit être dynamique. Pensez au traitement d'images, à l'apprentissage automatique, à la manipulation de graphes lourds et à d'autres tâches qui peuvent impliquer des centaines ou des milliers d'objets ou d'itérations de boucles si elles sont effectuées en PHP. Ces tâches seront plus rapides dans une bibliothèque précompilée.
  • Lorsque vous disposez d'une bibliothèque existante provenant de quelqu'un d'autre et que vous souhaitez l'exploiter. Historiquement, cela nécessitait l'écriture d'une extension PHP, ce qui est beaucoup plus difficile et impliqué que l'utilisation de FFI. Avec FFI, cependant, vous pouvez maintenant prendre une bibliothèque C existante, l'envelopper dans FFI, et l'utiliser à partir de PHP comme vous le feriez avec une extension. Les cas d'utilisation sont similaires : le traitement d'images, l'apprentissage automatique, et d'autres tâches intensives pour lesquelles il existe déjà une pléthore de code écrit en C ou C++. Dans ce cas, vous aurez certainement besoin d'écrire un fichier d'en-tête personnalisé ou d'utiliser FFIMe pour le relier à PHP. Je recommande fortement de construire une API plus agréable au dessus de FFI, comme nous l'avons fait dans cet exemple, pour faciliter l'utilisation par les utilisateurs finaux.

Si votre cas d'utilisation relève de l'un de ces domaines, vous disposez maintenant d'un nouvel outil pour vous attaquer au problème.

Gabriel Couto a rassemblé une série d'exemples d'utilisation de FFI avec des bibliothèques existantes pour lesquelles une réimplémentation en PHP n'aurait aucun sens. Maintenant que vous avez une bonne compréhension des bases de la FFI, les exemples ici offrent le niveau supérieur.

Liens utiles

Votre meilleur travail
est à l'horizon

Essai gratuit
Discord
© 2025 Platform.sh. All rights reserved.