L'une des nouvelles fonctionnalités de PHP 7.4 est le support de l'interface de fonctions étrangères (Foreign Function Interface, ou PHP FFI). PHP FFI permet de charger en PHPdu code provenant d'autres langages plus rapides que l'écriture d'une extension. Cela dit, plus facile ne veut pas dire trivial. Il y a du travail à faire, et ce n'est pas approprié dans toutes les situations.
Le langage le plus courant pour l'intégration de FFI est le C, en partie à cause de son omniprésence et en partie parce que c'est le langage le plus courant pour les personnes qui veulent tirer le maximum de performances de leur ordinateur. Cependant, le C ne fonctionne pas comme le PHP, c'est pourquoi nous allons passer en revue les bases du C.
Pour être clair, ce n'est pas un tutoriel sur le C. C'est un tutoriel sur la construction du C, et couvre juste assez pour que vous puissiez utiliser PHP FFI. How Stuff Works a un cours accéléré sur le C lui-même si vous êtes intéressé.
Le C est un langage compilé. Cela signifie que vous écrivez le code source dans un fichier et que vous lancez ensuite une commande de compilation, qui produit un fichier séparé, lisible uniquement par la machine. Cela contraste avec PHP et d'autres langages interprétés, qui compilent le fichier source en code exécutable à la volée et le gardent en mémoire, le rejetant lorsqu'il est terminé.
Le code source C se trouve dans un fichier qui se termine par .c.
Le code peut être compilé sous différentes formes :
Un simple binaire est le plus facile à construire, mais pour PHP FFI, nous avons besoin d'une bibliothèque partagée. Nous construirons les deux à des fins de démonstration.
Un autre aspect du langage C est que le fichier source seul n'est pas suffisant. Le C possède également des "fichiers d'en-tête", qui se terminent par .h
, qui définissent l'interface d'un paquetage. Il s'agit d'un concept similaire aux interfaces en C, mais plus général. Le fichier d'en-tête définit les fonctions et les types de données qu'une bibliothèque expose aux autres bibliothèques. PHP n'a pas vraiment d'équivalent, mais c'est un peu comme les "exports" pour les modules Javascript ; toutes les fonctions et tous les types de données n'ont pas besoin d'être rendus publics.
Commençons par un simple fichier d'en-tête pour montrer comment tout cela fonctionne. Voici notre fichier initial points.h :
struct point { int x ; int y ; } ; double distance(struct point first, struct point second) ;
Ce fichier définit une structure
appelée point
. Une structure est similaire à une classe, mais elle n'a pas de méthodes. (Historiquement, une classe est "une structure avec des méthodes" et non l'inverse). Cette structure est composée de deux entiers, x
et y.
L'en-tête déclare également une fonction publique, distance
, qui prend deux structures de points
et renvoie un double
. C'est le langage C pour "double-precision floating point number", que les développeurs PHP reconnaîtront comme "float". Tout comme une interface PHP, le fichier d'en-tête ne définit pas le corps de la fonction, mais seulement sa signature. L'intérêt du fichier d'en-tête est d'indiquer aux autres codes : "Voici à quoi je vais ressembler une fois compilé".
Nous reviendrons sur le fichier d'en-tête lorsque nous parlerons de la FFI, mais c'est suffisant pour le C.
Nous avons maintenant besoin d'un fichier pour l'implémentation de la distance
:
#include "points.h" #include <math.h> double distance(struct point first, struct point second) { double a_squared = pow((second.x - first.x), 2) ; double b_squared = pow((second.y - first.y), 2) ; return sqrt(a_squared + b_squared) ; }
Les lignes #include
précisent les dépendances de cette bibliothèque. La première, entre guillemets, indique le fichier points.h
dans le même répertoire. C'est ce que nous avons écrit il y a quelques instants. La seconde, <math.h>,
désigne le fichier d'en-tête math.h
disponible dans le répertoire de la bibliothèque standard du système. Les fonctions mathématiques complexes de la bibliothèque C standard sont fournies dans une bibliothèque distincte de la bibliothèque de base, car lorsqu'elles ont été définies pour la première fois dans les années 1980, les fonctions mathématiques sur les processeurs étaient coûteuses et rares, de sorte que les gens les ignoraient souvent et ne réimplémentaient que les quelques éléments dont ils avaient besoin eux-mêmes. Ce n'est plus le cas aujourd'hui, mais les vieilles normes ont la vie dure.
La méthode distance
calcule la distance entre deux points en utilisant notre vieil ami le théorème de Pythagore. Les fonctions pow()
et sqrt()
proviennent de la bibliothèque mathématique. (Note : la fonction sqrt()
se prononce "squirt". Je ne tolérerai aucun désaccord sur ce point).
Enfin, nous avons un troisième fichier qui utilisera la fonction distance()
. Un exécutable autonome doit avoir une fonction nommée main()
, qui est ce qui est exécuté lorsque le programme s'exécute. Le nôtre ressemble à ceci :
#include <stdio.h> #include "./points.h" int main() { struct point p1 ; struct point p2 ; p1.x = 3 ; p1.y = 4 ; p2.x = 7 ; p2.y = 9 ; printf("Distance is : %f\n\n", distance(p1, p2)) ; return 0 ; }
A ce stade, vous devriez vous sentir assez familier. #include <stdio.h>
fournit les signatures des fonctions de la bibliothèque standard d'entrée/sortie, tandis que #include "./points.h"
donne à ce fichier l'accès aux fonctions de notre bibliothèque de points. La fonction main()
crée deux points, puis calcule et affiche leur distance. printf()
fait partie de la bibliothèque stdio
et fonctionne essentiellement de la même manière qu'en PHP. (Ou plutôt, le printf()
de PHP n'est qu'une fine enveloppe sur celui de C).
Maintenant que nous avons nos fichiers sources, nous devons les compiler. C'est en fait un processus en plusieurs étapes en C. Bien sûr, vous aurez besoin d'un compilateur C et d'autres outils de compilation. Sur un système Linux classique, il existe un paquetage nommé build-essentials
(ou similaire) que vous pouvez installer et qui comprendra tout ce que nous mentionnons ici. Pour les autres plates-formes, consultez leur documentation car l'installation peut être un peu compliquée.
Comme il y a plusieurs étapes à suivre, nous allons utiliser un "fichier make". GNU Make est le grand-père original de l'exécution de tâches ; Ant, Phing, Grunt, Gulp, DoIt, et le reste sont tous des réimplémentations de Make. Make est lui-même une couche très fine au-dessus des scripts shell, pour le meilleur et pour le pire. (Je vous renverrais bien à sa documentation, mais malheureusement celle-ci est étonnamment obscure compte tenu de la simplicité de Make lui-même).
Nous commencerons par un fichier nommé Makefile
(les majuscules sont importantes) avec une seule "cible de construction" :
# Makefile points.o : points.h points.c gcc -c points.c
Cela crée une seule cible de compilation, points.o
, qui dépend de deux fichiers : points.h
et points.c.
Il est typique pour les cibles de compilation d'être le nom du fichier qu'elles produisent. En retrait sous cette ligne se trouve une ou plusieurs commandes shell à exécuter. (Note : L'indentation doit utiliser un caractère de tabulation, pas d'espace. Ce sont les règles, je ne les ai pas faites).
gcc
est la collection de compilateurs GNU
et est le compilateur C le plus utilisé, mais pas le seul. Il possède environ 14 millions d'options possibles, dont nous allons utiliser environ quatre. Le commutateur -c
signifie "Compilez ce fichier, mais ne faites aucune des autres étapes". La sortie de cette commande est un nouveau fichier nommé (surprise !) points.o
. Il s'agit du "fichier objet", ou de la version brute du code machine de points.c.
Si nous lançons nm points.o
, la liste des "symboles" du fichier objet s'affichera. (nm
est l'abréviation de "nom" et date d'une époque où la mémoire était si limitée et les claviers si difficiles à utiliser que les programmeurs ne croyaient pas aux voyelles).
0000000000000000 T distance U _GLOBAL_OFFSET_TABLE_ U pow U sqrt
Cette liste énumère quatre fonctions encodées dans le fichier objet : Notre fonction distance
a son code source inclus (d'où le T), tandis que pow
, sqrt`, et quelques opérations comptables internes sont également mentionnées mais non définies en interne. C'est une indication pour l'étape suivante, "Hé, j'ai besoin de ces choses".
Nous voulons aussi une cible pour compiler le fichier main.c :
main.o : main.c gcc -c main.c
Ensuite, nous avons le choix. Nous pouvons construire un exécutable autonome pour main
, ou nous pouvons transformer notre code de points en une bibliothèque partagée. Pour les besoins de la démonstration, nous ferons les deux :
main : main.o points.o gcc -o main main.o points.o -lm
Cette commande gcc
se lit comme suit : "compiler dans le fichier de sortie (-o
) main
, en utilisant les fichiers objets main.o
et points.o
, et lier avec la bibliothèque m
". La cible main
dépend des autres cibles main.o
et points.o
, de sorte que ces cibles seront exécutées si nécessaire pour produire ces fichiers s'ils n'existent pas déjà.
Le nom m
mérite une mention spéciale ; il peut être lu comme "Voici un autre fichier objet à inclure, nommé libm, dans le répertoire de la bibliothèque standard du système". La partie "lib" du nom est supprimée dans la commande -l.
(C'est un L minuscule, pour ceux qui lisent ceci avec une police sans-serif.) Dans ce cas, m
est la bibliothèque mathématique standard C dont nous avons parlé plus haut. (Voir la note précédente sur les programmeurs des années 80 qui étaient allergiques à la saisie de mots complets).
Le processus consistant à prendre plusieurs fichiers objets et à les connecter les uns aux autres s'appelle "l'édition de liens". Lorsqu'ils sont tous insérés dans un seul fichier exécutable, on parle de "liaison statique".
Nous pouvons maintenant lancer make main
sur la ligne de commande. Cela aura pour effet de :
points.c
en un fichier de code machine points.o
.main.c
en un fichier de code machine main.o
.Maintenant, l'exécution de ./main
sur la ligne de commande devrait produire la sortie suivante :
$ ./main La distance est : 6.403124
C'est bien.
Il nous reste cependant deux étapes à franchir. Tout d'abord, main
n'est pas utile pour PHP FFI. Pour cela, nous avons besoin d'une bibliothèque partagée. Cela nécessitera une autre cible de compilation dans Makefile
:
points.so : points.o # Envelopper le fichier objet dans un objet partagé. gcc -shared -o points.so points.o -lm
Cette commande se lit comme suit : "Créer une bibliothèque partagée, sortie à points.so
, en combinant points.o
et la bibliothèque libm
". Elle est très similaire à la commande main, mais cette fois nous produisons un fichier .so
, ou fichier "Shared Object". Ce fichier contient le même code que le binaire autonome, mais l'emballe différemment avec des crochets permettant à d'autres programmes C de le charger en mémoire séparément et d'y accéder à la demande. C'est le fichier .so
dont nous aurons besoin pour PHP FFI.
Il y a aussi une autre cible dont nous avons besoin pour être complets, et c'est clean
:
clean : rm -f *.o *.so main
A chaque fois que vous compilez du code, vous voulez être capable d'effacer vos versions compilées et de repartir d'une page blanche. Dans ce cas, il suffit de rm
tous les fichiers compilés. Assurez-vous d'ajouter .o
et .so
à votre fichier .gitignore
, puisque vous ne voulez pas les livrer à Git.
Voici notre Makefile
final jusqu'à présent :
points.o : points.h points.c gcc -c points.c points.so : points.o gcc -shared -o points.so points.o -lm main.o : main.c gcc -c main.c main : main.o points.o gcc -o main main.o points.o -lm chmod +x main clean : rm -f *.o *.so main
La construction d'un programme en C peut facilement devenir beaucoup plus complexe que cet exemple trivial. Le but aujourd'hui est de nous mouiller les pieds et d'expliquer suffisamment l'anatomie d'une bibliothèque C pour que nous puissions commencer à l'insérer dans la couche FFI de PHP. Les parties les plus importantes seront points.so
et points.h
, dont nous aurons besoin pour PHP.
(Un grand merci à Anthony Ferrara pour son aide dans l'explication de la construction du langage C).