PHP est depuis sa création fortement lié au développement de sites internet. Pourtant, depuis PHP 4.3 (2002) qui a ajouté la SAPI CLI, PHP peut servir pour développer des applications ainsi que des services. Avec l’arrivée de FFI en 7.4, PHP peut maintenant appeler des fonctions C directement. Cette nouveauté permet d’utiliser des bibliothèques externes sans nécessiter d’extension PHP. Elle ouvre ainsi les portes de la programmation système à PHP.
Nous vous présenterons comment nous avons utilisé FFI en production sur un projet domotique (contrôle d’une salle de Karaoké). Pourquoi avons-nous fait ce choix ? Quelles problématiques avons-nous rencontrées ? Pour quel résultat ?
Finalement: FFI production ready ?
_ Bonjour à tous. Bienvenue à ma conférence qui s'intitule FFI : De nouveaux horizons pour PHP. L'idée de cette conférence, c'est de savoir un peu si FFI est une nouvelle fonction pour PHP comme il y en a régulièrement, ou si elle permet d'ouvrir le champ des possibles. L'idée de cette conférence est de montrer un cas pratique d'usage des FFI qu'on a eu chez KaraFun et comment nous l'avons solutionné.
Je m'appelle Pierre Pelisset. J'étais développeur back chez KaraFun depuis sept ans. Je suis devenu CTO depuis quelques semaines. Je suis développeur depuis très longtemps. J'ai toujours fait du PHP. Mais j'ai un peu regardé ailleurs et c'était très intéressant. J'ai appris à faire du C.
FFI, c'est quoi ? Foreign function interface. C'était une extension de PHP. Ça a été intégré dans le corps depuis. L'idée, c'est de pouvoir effectuer des fonctions de langage cible depuis un langage source. On peut effectuer des fonctions C depuis PHP. C'est nouveau dans PHP, mais c'est dans d'autres langages. Python, C++, quand vous êtes en externe C, vous pouvez utiliser FFI.
Comment ça s'utilise ? Si vous voulez appeler une fonction C dans FFI. On lui passe le prototype de la fonction, avec tous les headers, ce que vous voulez utiliser dans le projet FFI. On va définir la fonction...
(coupure de son)
... code pow qui va appeler votre fonction C. Vous obtiendrez le résultat.
FFI, de base, il gère tout ce qui est de type simple. Les doubles. Ce genre de choses. Mais tout ce qui est une chaîne de caractères ou pointeurs, vous allez devoir utiliser des fonctions spécifiques dans FFI pour pouvoir le passer en C, parce que ce n'est pas le même système de gestion entre PHP et langage C.
À quoi cela sert tout ça ? Pour faire le Binding d'une librairie sans passer par une extension. Vous avez une librairie C ou dans un autre langage compilé. Vous voulez appeler les fonctions de cette librairie dans PHP sans créer une extension. C'est ce qui a été fait pour un portage vers PHP de tensor flow qui a été fait par FFI.
Optimisation des performances du code fonctionnalité avec du code native. À un endroit, vous avez un problème de performance sur une partie spécifique. Exemple, traitement de l'image. PHP n'est pas forcément la meilleure solution pour faire du traitement de l'image. Ça peut être long. Vous allez écrire une fonction en C ou C++ pour faire la fonction d'image. L'intégralité du système reste en PHP sauf la partie critique.
Le dernier cas d'usage, c'est celui que je vais creuser. L'accès aux fonctions du système. En PHP, de base, nous avons accès à quelques fonctions système. Il y a une fonction PCN PL. Tout n'est pas disponible. Sur les systèmes UNIX, nous pouvons y accéder via PHP.
Je vais vous présenter le cas d'usage que nous avions chez KaraFun. La problématique que nous avions. Ce qui nous a amenés à utiliser FFI pour trouver une solution. KaraFun, nous avons une application de divertissement musical. Nous proposons du karaoké entre autres, et nous avons ouvert aussi les KaraFun Bars. Les box de karaoké privatif. Offrir à nos clients une expérience qui se rapproche fortement comme s'ils étaient la star d'un concert live. Ils sont avec leurs amis, ils chantent, ils ont le karaoké qui apparaît sur leur télé. Ils ont de la musique, ils peuvent régler le volume. Ils ont des jeux de lumière qui sont automatisés en fonction de la musique et du moment de la chanson. C'est aussi un bar classique. Nous avons tout ce qui est interface de commande. Nous avons un système de paiement dans la salle.
Tout ça, c'est fait en PHP. Nous utilisons l'application KaraFun pour iPad. Sinon, c'est fait par des mo.php qui tournent par un ordinateur par salle de karaoké. Pour tout ce qui est gestion automatique de la lumière, on doit envoyer à un boîtier qui gère les lumières derrière.
Au début, le boîtier, on pouvait l'utiliser via UDP. On envoyait des paquets. C'était simple. Pour simplifier la maintenance dans nos KaraFun bar, on a décidé de mettre à la place un boîtier qui se commande par USB. C'est très simple. Pour les bars, ils installent cela, c'est pratique. Par contre, nous, nous avons tout fait en PHP. Communiquer vers de l'USP, ce n'est pas très pratique. La communication se fait via RS-232, le port série. Il y a une librairie historique qui va utiliser toutes les fonctions shell_exec pour configurer le port série. Mais pour nous, pour le boîtier, ça ne fonctionnait pas. Ce qui s'est passé, c'est que nous avons utilisé PySerial. Nous avions un script python pour ouvrir le port série. Pour faire l'écriture via PHP. C'était il y a quelques années. À l'époque, FFI n'existait pas. Avec le temps, nous sommes arrivés : est-ce qu'on pourrait pas avoir une alternative à PySerial uniquement en PHP ? PySerial, ils ont surtout des appels du système pour configurer le port série. On pourrait le faire avec PHP via FFI.
Donc, quand nous utilisons un port série, tout ce qui est lecture et écriture, ça se fait de la même façon que sur un fichier classique. Vous allez faire Fright et Fread pour lire et écrire dedans. Vous allez devoir configurer votre port série. La vitesse de l'échange, la configuration du Byte de stop. Il faut le configurer pour que l'élément à l'autre bout de votre port série comprenne ce que vous voulez lui dire. Tout cela sur les systèmes Unix, ça passe par une application qui s'appelle Termios. L'idée, c'est de vous montrer comment on a fait un port du API vers PHP pour l'utiliser via FFI.
Ça permet de configurer un terminal, un port série, une imprimante, un boîtier, etc. On va le configurer via des options qui sont des flags. L'exemple le plus connu déterminé à ce que vous avez peut-être, c'est quand vous utilisez la commande, vous connectez sur le serveur, vous composez le mot de passe, il ne va pas apparaître, via Termios, nous avons désactivé le flag qui s'appelle écho. Ce qui a été écrit n'apparaît pas à l'écran. Après, on pourra complètement le faire avec le Code suivant où on récupère la configuration de notre entrée standard avec Termios et Attribute, on va désactiver le flag echo, on ira sur l'entrée standard.
Pour ce faire, la première chose à faire, on va créer un objet Termios qui va reprendre la structure qu'on aurait dans la structure C de l'API Termios. Un tableau de caractère de contrôle, deux hints pour la vitesse de lecture, la vitesse d'input.
Après, nous allons créer la fonction pour récupérer la configuration en les passant en file descriptor qui est un hint. J'en parlerai plus dans la deuxième partie.
Maintenant, nous arrivons au moment où nous allons devoir définir les éléments de C qu'on va appeler. On va créer un fichier .h. On va définir la liste de ce qu'on va utiliser. Nous avons la structure Termios. Les quatre flags, les caractères de contrôle.
Nous allons aussi utiliser les fonctions pour récupérer nos informations. Nous allons définir la fonction tcgetattribute. Elle prend un file descriptor et un pointeur vers la structure Termios. Nous avons aussi deux Gator vers les vitesses.
En tout premier, comme montré au tout début sur l'usage de FFI, nous allons devoir commencer à récupérer notre objet FFI qui correspond aux points H. Je l'ai fait dans une méthode intermédiaire. On va vérifier si on ne l'a pas défini. On va créer un seul objet FFI. Ce sera plus rapide et on évitera d'en recréer un chaque fois.
On va pouvoir récupérer la configuration de notre port. Nous récupérons l'objet FFI. Nous allons faire une allocation dynamique. Nous allons lui demander de créer une structure Termios. Elle va être stockée dans la variable PHP. C'est un pointeur vers une variable C. On va pouvoir appeler la fonction C vers la fonction data attribute. La fonction C attend un pointeur. Nous allons utiliser la méthode adresse pour le transformer en pointeur.
Une fois que c'est fait, l'objet pointeur est rempli. Nous pourrions nous arrêter là, mais nous y retournerons un objet de type C data à l'extérieur. Nous allons appeler une méthode toute simple qui va copier les données de notre C data vers notre objet Termios qui est un vrai objet PHP. Nous allons appeler notre Gator pour la vitesse. Pour récupérer les inputs et l'output speed. L'allocation dynamique que nous avons faite ici, il sera nettoyé.
Nous allons copier les données chaque fois. Les objets Cdata, nous ne pouvons pas les utiliser comme n'importe quelle variable PHP. Même si ça contient un tableau, vous n'allez pas pouvoir faire d'efforts dessus.
Maintenant que nous avons récupéré notre configuration de notre terminal, nous allons passer à la deuxième partie où nous allons vouloir modifier la configuration de notre terminal. Ça se fait de la même manière. Nous allons définir ctgetattribute. La seule différence sur ça, c'est que nous avons un périmètre d'action pour choisir à quel moment on fait l'action. Nous allons pouvoir définir notre méthode PHP, tcgetattribute. Elle fait l'inverse de la fonction précédente. On crée la structure Termios. On va appeler nos deux setteurs. Pour que les C data soient modifiés. Si on a différents zéros, c'est parce que ça a planté.
C'est simple, c'est la fonction hero.
Dans l'exemple en haut, nous avions déjà activé l'affichage, nous avions utilisé la constante. Toutes ses valeurs sont définies directement dans les fichiers Header de Termios. Ce ne sont pas des constantes, ce sont des macros. Si vous aviez des constantes, il y aurait de la mémoire pour compiler la valeur. Mais quand ce sont des macros, c'est la valeur qui est intégrée à la place de la constante condition. Pour le récupérer, j'ai fait un script PHP qui va sortir toutes les données. Dans la classe, nous allons pouvoir définir toutes les constantes.
C'est quelque chose que je n'avais pas prévu. Ces constantes, sur une plate-forme ou sur une autre, ce n'est pas forcément la même valeur. Si vous prenez le Flag eCanon qui permet d'accéder en mode brut caractère par caractère sur un fichier, par exemple, sur Linux, la valeur, c'est deux. Mais sur Darwin, Mac OS, c'est 100. Si on souhaite supporter plusieurs plates-formes, on va devoir gérer ce genre de cas.
(coupure de son)
... Ce sera totalement transparent pour l'utilisateur final. Nous allons pouvoir tester notre programme. Nous le lançons. Dommage. C'est un peu le problème de FFI. Nous touchons à la mémoire. Vous avez les inconvénients du C aussi. Ce qui s'est passé, nous avons pris le header de Termios, mais sous Linux ou sous Mac, ce n'est pas les mêmes. Tous les Flags sont 32 bits. Mais sous Mac, c'est du 64. Il a essayé d'accéder à des fonctions mémoire qui n'existaient pas. Ça a planté. Pour ce faire, j'ai fait une sorte de moteur de template sur mon Termios.h. Dans mes plates-formes spécifiques, j'ai défini des valeurs spécifiques pour chaque plate-forme. Derrière, nous testons. Tout marche bien. Nous sommes contents.
Quand on récupère et qu'on modifie la configuration de terminale, on devait passer en paramètre un file descriptor. C'est un identifiant d'un fichier ouvert par un programme. Tout programme qui selon, on en a au moins trois, 0, 1 et 2 pour l'entrée standard, et le système d'erreur. C'est sur Unix. Mais en PHP, on n'utilise pas les file descriptors, mais les streams. Dans le moteur PHP, il y aurait le file. Il faut récupérer tout cela. PHP ne propose pas de fonction standard pour le faire. Nous allons essayer d'accéder aux données internes du moteur de PHP. Via le Code C. On va le compiler. On va le charger en FFI. On va pouvoir le récupérer. Récupérer notre feuille descriptor.
Pour toute cette partie, je me permets un avertissement. J'ai récupéré les éléments en allant dans les sources de PHP. Il y a peut-être des choses qui ne sont pas exactes ou les meilleures façons de faire aujourd'hui. Mais ce sont des éléments que j'ai trouvés.
Comme je le disais au début, nous passons une variable via FFI pour appeler une fonction C, nous allons récupérer directement la valeur de la variable. Si vous passez une variable qui contient 8, la fonction va recevoir 8. Nous sommes dans le contexte d'exécution de PHP. Nous avons toutes les fonctions de base. Nous allons appeler une fonction en lui passant le nom de la variable qu'on veut récupérer. On va créer une table qui sont dans toutes les variables qui sont dans le courant. Si on a trouvé et que c'est un type ressource, on va creuser pour récupérer le fileno. On va devoir faire du nettoyage.
Pour récupérer les primes une fois qu'on a le zval, on va devoir extraire un PHP stream avec cette fonction. Si elle revient nulle, c'est que ce n'est pas un stream. Sinon, il faut la caster select. Si c'est C, on récupérera un numéro de fichiers qu'on pourra retourner.
Derrière, on compile tout cela avec PHP, et on compile un PSO. On peut faire un FFI PDF en lui passant le point SO à charger. Sur l'objet FFI, on pourra appeler notre fonction le nom de la variable qu'on a en paramètre. Donc, pour STDIN, c'est zéro. etc. Mais il y a des inconvénients. Nous avons été obligés de faire zéro. Nous allons été obligés de le faire pour chaque système. Linux, Mac, Windows, et chaque infrastructure. Est-ce qu'il n'y a pas une solution plus simple ? Le faire entièrement en PHP ? Parce que finalement, toutes les fonctions du noyau sont déjà chargées. Nous sommes déjà dans l'environnement de PHP. Si nous chargeons les non-headers, nous pouvons les appeler en FFI depuis PHP. Pour récupérer un zval, on peut juste récupérer notre table via PHP et retrouver la valeur. Pour la version complète, chaque fois que PHP API : si ce n'est pas une ressource, on s'arrête. Sinon, on récupère la ressource, on fait le stream, on récupère le cast.
On arrive à la phase finale. La bibliothèque. Nos deux dépendances, on peut les imposer par Composer. On n'est pas obligé de passer par pickle et de compiler. Pour la création de la classe Serial, j'ai repris les mêmes paramètres. Nous avons en plus un stream et un file descriptor. Je récupère le file descriptor pour que le file serie comprenne ce qu'on veut dire. Je commence par Termios pour récupérer les informations, la configuration finale, je vais l'appliquer. Pour le Baud Rate, la vitesse de lecture, celui que je veux utiliser, c'est 9600, par exemple, ça se trouve dans une constante qui est nulle si pas supportée. Je récupère la valeur de la constante. Si elle n'est pas définie, c'est que mon système ne supporte pas. Sinon, je vais la définir dans espeed.
Pour la lecture et l'écriture. C'est tout simple. Pour le read, on fait du fread. Pour write, on fait du fwrite.
Finalement, ça a marché. Je m'étais dit que ça n'allait jamais fonctionner en prod. Au début, nous avons été... nous avons fait attention. Nous ne l'avons mis que dans deux salles. Et finalement, au bout d'une semaine, nous n'avions aucun retour négatif. Des retours des gestionnaires qui disaient que rien ne fonctionnait. Quand nous avons regardé les logs, il n'y avait rien. Top. Derrière, nous avons mis cela partout. Ça fait deux mois que ça tourne partout. Nous n'avons eu aucun souci. Nous avons pu virer la totalité de ce que nous avions en Python. Vu que tout ce qu'on avait en Python, c'était un bout de scotch, nous n'étions pas mécontents.
Si c'était à refaire, est-ce qu'on a eu raison de le faire en FFI ? Est-ce qu'on le referait ? Il y a beaucoup de systèmes pour faire une extension PHP directement. Ça a l'avantage d'être beaucoup rapide et plus simple à faire. Nous avions aussi la grande chance d'avoir un environnement maîtrisé. Nous savions sur quel matériel ça allait tourner. Sur quelle architecture, quel système d'exploitation. Si nous avions été sur quelque chose de très hétérogène, peut-être que nous n'aurions pas fait ce choix. Mais pour l'usage FFI, les serveurs, vous savez sur quoi ils tournent. Mais avec le cloud, c'est peut-être un peu moins le cas. Mais ça peut être une solution.
Si je devais résumer rapidement les avantages et les compagnons. L'avantage : La simplicité et la rapidité de développement. Vous pouvez déployer tout cela par Composer. C'est vraiment exceptionnel. Pour du prototypage, puisque c'est simple et rapide à faire, c'est parfait. Ça répond à la majorité des besoins. Sans FFI, nous aurions dû faire une extension PHP ou nous n'aurions pas pu le faire. Et nous aurions dû trouver une autre solution que PHP. Le point négatif : C'est plus lent qu'une extension. Le premier appel est lent. Je pense que c'est à cause d'un système de chargement. Mais les appels suivants sont aussi rapides que les appels de PHP. C'est interprété et pas compilé. Quand vous utilisez des macros, ce n'est pas compliqué. Pour la plupart des cas, ce n'est pas un souci.
Voici les différents QR codes. Si vous voulez voir les différentes démos qui ont été faites. La plupart du code a déjà été montré ici. Mais si vous voulez en voir plus, vous pouvez aller voir sur ces trois projets.
C'est tout pour moi. Merci beaucoup à tous. N'hésitez pas à me faire vos retours. C'était ma première conférence tech. J'attends vos retours avec impatience pour m'améliorer pour une éventuelle prochaine fois.
(applaudissements)
_ Merci beaucoup. Nous sommes larges sur les questions. Profitez-en. Nous avons cinq grosses minutes pour faire cela. Qui veut commencer ?
Non ? J'en pose une pour initialiser le truc. Sans aller dans le comparatif. Est-ce que tu as regardé Wazam, ce qui se faisait ? L'écosystème qui émerge ?
_ Pierre Pelisset : C'est très intéressant. Nous commençons à l'utiliser pas mal chez KaraFun notamment pour faire un portage de tout ce qui est code C++ pour pouvoir l'utiliser dans un environnement de navigateur. C'est top.
_ Est-ce que quelqu'un a été inspiré entre-temps ? Non ? Mais si !
_ Merci pour la conférence. C'est une bonne première fois. Ma question, c'est à quel point est-ce que votre équipe DEV a dû apprendre à faire du sourcing ? Comme Go qui sont plus faciles à prendre en main. Vous êtes-vous posé la question : On arrête PHP et on va plutôt sur Go pour faire des appels système comme vous faites. Mais on fait tout cela propre. Avec un seul langage. Plutôt que de prendre du C pour faire du PHP.
_ Nous avions déjà une base de communication existante sur ce projet avant d'avoir ce besoin des FFI. Nous avions déjà une grosse base en PHP qui existait. La question de tout refaire dans un autre langage, elle ne s'est pas posée très longtemps. Mais côté C, il faut connaître les bases et les mécanismes. Mais nous sommes plusieurs DEV dans l'équipe. À en avoir fait dans notre formation initiale. Nous avons surtout ravivé les souvenirs. Pour moi, il n'y a pas besoin d'être un développeur C accompli pour utiliser FFI. Il faut connaître les principes de base. Il y a des headers. On peut faire des pointeurs. Un stream en C, c'était un pointeur de char*. Il n'y a pas besoin de faire un truc complexe en C pour pouvoir utiliser FFI.
_ Une autre question ?
_ Test. Puisque vous utilisez des API internes. Est-ce que vous avez des tests automatisés comme pour le reste ? Est-ce qu'il y a des contraintes spécifiques pour cela ?
_ La question des tests automatisés là-dessus, c'est complexe. Nous sommes liés à des matériels et à des ports séries. Nous n'avons pas trop de systèmes automatiques. J'ai regardé comment ils avaient fait PySerial. S'ils avaient réussi à automatiser des choses. Ils ont dit : Vous prenez un port série, vous bidouillez pour que l'input reparte au output. Vous lancez les tests. Si ça passe, c'est bien. Ça ne nous paraissait pas super concluant. Mais ce sont toujours des tests automatisés. Mais pour les lancer automatiquement, pour rentrer dedans, il y a quand même une petite barrière matérielle.
_ Une petite dernière pour la route ?
Non ? Je vous propose de profiter d'un peu de temps libre. Et à vous diriger dans une autre salle. Il y a les applaudissements qui sont passés à côté. Attendez dans le couloir. Ça va s'ouvrir.
(applaudissements)
Commentaires