Des extensions PHP en Go avec FrankenPHP
Publié le 25 août 2025
Lors du PHPVerse 2025, la conférence organisée par JetBrains à l’occasion des 30 ans du langage PHP, notre coopérateur Kévin Dunglas a fait une annonce inédite : la possibilité de créer des extensions PHP avec le langage Go grâce à FrankenPHP. Bien que cette nouveauté n’en soit pas vraiment une car cela a toujours été possible depuis les débuts du projet, nous mettons aujourd’hui en lumière de manière plus complète cette petite révolution et faisons en sorte de la rendre plus accessible pour tout le monde. Voyons ensemble comment !
Premières explorations
Les extensions sont des bouts de code qui s’attachent directement sur l’interpréteur de PHP et permettent de faire à peu près tout ce que vous voulez. La raison principale est que les extensions sont écrites en C ce qui donne donc un accès haut et bas niveau à votre machine. Parmi les extensions les plus connues, nous pouvons citer Parallel qui rend possible l’exécution de code parallèle en PHP, GD pour la gestion d’image ou encore PHP Redis pour la communication avec les serveurs de cache Redis (et leurs alternatives comme Snapchat KeyDB).
Plus récemment, nous avons vu l’apparition d’extensions écrites en Rust et C++ (bien que ce second choix existe depuis longtemps, il n’est pas si populaire). Si vous souhaitez vous faire une idée, Packagist recense les extensions installables avec PIE et leur langage d’écriture. Tous ces langages restent cependant très bas niveau et il n’est pas toujours évident de les appréhender. C’est encore plus vrai pour le développement d’extensions PHP, qui exigent souvent une connaissance assez poussée des rouages internes de PHP. FrankenPHP étant écrit dans un langage plus haut niveau, Go, pourquoi ne pas en profiter pour essayer d’écrire des extensions PHP dans ce langage ? En effet, FrankenPHP intègre le runtime Go, il est donc tout à fait légitime d’imaginer utiliser ce langage sans limitation technique particulière autre que le fait que ça n’a jamais été fait.
Avec Kévin, nous avons alors commencé à explorer les possibilités et à travailler sur un Proof Of Concept où une nouvelle fonction native serait ajoutée à PHP pour exécuter du code Go. Après quelques jours de recherches, les résultats étaient là : une goroutine (un morceau de code exécuté en parallèle du code principal) était lancée à partir de PHP ! La magie de tout ça vient de l’existence de la bibliothèque CGO, permettant à du code C d’appeler du code Go, mais aussi d’appeler du code C depuis Go. À partir de là, nous sommes prêts à développer n’importe quelle fonctionnalité.
FrankenPHP comme bibliothèque utilitaire
Une des choses qui saute aux yeux suite à ces expérimentations est que, malgré tout, on passe encore beaucoup de temps à écrire du code C. Cela vient principalement de trois raisons :
- L’enregistrement de l’extension au sein de PHP se fait obligatoirement en C car cela nécessite de jouer avec certains pointeurs internes du Zend Engine. La bonne nouvelle est que ce code est majoritairement le même dans toutes les extensions ;
- Le jonglage de type entre C et Go. Beaucoup de types de variables sont compatibles entre C et Go et peuvent être utilisés directement. C’est le cas des entiers, des nombres à virgule flottante ou encore des booléens par exemple. Cependant, les structures plus complexes comme les chaînes de caractères, les objets et les tableaux nécessitent une conversion et ne peuvent pas être utilisés tels quels. Par exemple, dans l’interpréteur PHP, les chaînes de caractères sont représentées par une structure contenant à la fois les données et la taille de la chaîne. Il faut donc explorer les fonctionnements internes de PHP qui sont parfois peu documentés. Les LLMs sont déjà excellents pour expliquer certains mécanismes obscurs du moteur de PHP, mais cela nécessite tout de même des connaissances bas-niveau et le risque de corrompre la mémoire reste élevé.
- Du code C doit être écrit pour appeler le code Go, quoi qu’il arrive.
FrankenPHP possède ici une belle carte à jouer : celle de la bibliothèque utilitaire pour éviter aux développeurs ces trois problématiques. Il est évidemment possible de le faire à la main, et de la documentation existe pour expliquer chaque étape, permettant de bien comprendre les étapes du développement d’une extension from scratch. Il est cependant possible de s’abstraire de ces étapes.
Pour le premier point, il faut savoir que les extensions écrites en Go seront donc des Go modules, et plus précisément des modules Caddy dans ce cas. FrankenPHP utilise Caddy comme serveur web intégré, et ce dernier permet l’intégration de modules personnalisés en une ligne. Les modules personnalisés peuvent notamment prendre la forme d’un fichier Go avec une fonction init()
qui est exécutée au démarrage du module par Caddy. C’est l’endroit parfait pour enregistrer une extension ! Grâce aux récentes contributions au projet, FrankenPHP expose une méthode RegisterExtension()
qui permet de s’abstraire de tout le code C requis pour enregistrer une extension. Sans rentrer dans tous les détails (qui sont disponibles dans la documentation si vous êtes curieux·se !), l’enregistrement d’une extension en Go ressemble à ceci :
package ext
/*
#include "ext.h"
*/
import "C"
import "github.com/dunglas/frankenphp"
func init() {
frankenphp.RegisterExtension(unsafe.Pointer(&C.ext_module_entry))
}
Pas une ligne de C n’a été écrite ici, et l’extension est bien enregistrée !
En ce qui concerne le jonglage de type, FrankenPHP propose à nouveau des fonctionnalités exposées pour vous abstraire des mécanismes internes de type. Nous l’avons vu un peu plus haut, certains types scalaires n’ont pas besoin de conversion. Mais d’autres si, comme les chaînes de caractères. C’est pourquoi FrankenPHP propose des méthodes comme frankenphp.GoString()
(pour récupérer une string Go à partir d’une string C) et frankenphp.PHPString()
(pour convertir une string Go en string utilisable par PHP) qui s’occupent de faire les conversions automatiquement. Nous espérons pouvoir ajouter de nouvelles méthodes utilitaires au fur et à mesure pour d’autres types de données. Notez que les tableaux sont supportés aussi, et le support des callables en tant qu'argument est en cours de développement.
À nouveau, cet article n’effleure que la surface de ces nouveautés, expliquées exhaustivement dans la documentation complète d'ores-et-déjà en ligne.
Vous l’avez compris, FrankenPHP joue le rôle de facilitateur de création d’extensions PHP en proposant des outils simplifiant grandement le code de vos extensions. Il reste cependant une dernière problématique à résoudre : l’écriture du code C permettant l’appel du code Go. Cela nous mène directement à la section suivante !
Le générateur d’extension
Conscients des dizaines, voire des centaines de lignes de code C à écrire pour faire le “passe-plat” entre C et Go, nous avons réfléchi à la prochaine étape. Serait-il possible de créer un générateur d’extension PHP, qui prendrait en entrée un simple fichier Go, peut-être avec une syntaxe particulière, et qui s’occupe du reste ? L’excellente nouvelle est que la réponse est oui ! Après plusieurs semaines de développement, un nouvel outil est intégré au sein de FrankenPHP en tant que sous-commande : le générateur d’extension. L’objectif principal est clair : la développeuse ou le développeur doit pouvoir compiler et intégrer une extension PHP sans jamais écrire une ligne de code C.
Le générateur définit des directives Go spécifiques. Voyez les directives Go comme les annotations ou les attributs en PHP. Voici un exemple de fichier Go compatible avec le générateur d’extensions :
package main
// export_php:function multiply(int $a, int $b): int
func multiply(a int64, b int64) int64 {
return a * b
}
// export_php:function is_even(int $a): bool
func is_even(a int64) bool {
return a%2 == 0
}
// export_php:function float_div(float $a, float $b): float
func float_div(a float64, b float64) float64 {
return a / b
}
Grâce aux directives // export_php:function
suivies de la signature de la fonction, le générateur va s’occuper de définir tout le code C intermédiaire. Il ne reste plus qu’à passer ce fichier au générateur :
alex@alex-macos frankenphp % frankenphp extension-init ext-dir/ext.go
2025/06/20 09:49:09.273 INFO PHP extension "ext" initialized successfully in "ext-dir/build"
alex@alex-macos frankenphp % ls -la ext-dir/build
total 48
drwxr-xr-x@ 8 alex staff 256 Jun 20 11:49 .
drwxr-xr-x@ 8 alex staff 256 Jun 20 11:49 ..
-rw-r--r--@ 1 alex staff 418 Jun 20 11:49 README.md
-rw-r--r--@ 1 alex staff 1673 Jun 20 11:49 ext.c
-rw-r--r--@ 1 alex staff 396 Jun 20 11:49 ext.go
-rw-r--r--@ 1 alex staff 226 Jun 20 11:49 ext.h
-rw-r--r--@ 1 alex staff 168 Jun 20 11:49 ext.stub.php
-rw-r--r--@ 1 alex staff 865 Jun 20 11:49 ext_arginfo.h
FrankenPHP a créé tous les fichiers nécessaires aux extensions PHP :
- Un fichier README.md contenant les éléments exportés pour l’extension ;
- Un fichier C contenant le code intermédiaire que nous voulions éviter d’écrire ;
- Un fichier Go, très similaire à celui d’origine mais légèrement modifié pour fonctionner correctement avec le code C ;
- Un fichier de header contenant la définition de certaines fonctions permettant au code C et Go de bien fonctionner ensemble ;
- Un fichier stubs avec la définition des signatures des fonctions PHP ;
- Un fichier arginfo contenant toutes les directives nécessaires pour enregistrer les nouvelles fonctions dans le Zend Engine de PHP.
Nous l’avons vu un peu plus haut, Caddy permet l’ajout de modules personnalisés supplémentaires pour étendre ses fonctionnalités : il faut donc donner le répertoire build
à Caddy et il s’occupera du reste. L’extension est fonctionnelle et aucune ligne de C n’a été écrite à la main !
La force de ce générateur est le support d’autres fonctionnalités importantes. Voici un exemple plus complet de fichier compatible :
package main
// export_php:namespace Go\MyExtension
import (
"C"
"github.com/dunglas/frankenphp"
"strings"
"unsafe"
)
// export_php:const
const MY_GLOBAL_CONSTANT = "Hello, World!"
// export_php:classconst MySuperClass
const STR_REVERSE = iota
// export_php:classconst MySuperClass
const STR_NORMAL = iota
// export_php:class MySuperClass
type MyClass struct {
Name string
}
// export_php:method MySuperClass::setName(string $name): void
func (mc *MyClass) SetName(v *C.zend_string) {
mc.Name = frankenphp.GoString(unsafe.Pointer(v))
}
// export_php:method MySuperClass::getName(): string
func (mc *MyClass) GetName() unsafe.Pointer {
return frankenphp.PHPString(mc.Name, false)
}
// export_php:method MySuperClass::repeatName(int $count, ?int $mode): void
func (mc *MyClass) RepeatName(count int64, mode *int64) {
str := mc.Name
result := strings.Repeat(str, int(count))
if mode != nil && *mode == STR_REVERSE {
runes := []rune(result)
for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
runes[i], runes[j] = runes[j], runes[i]
}
result = string(runes)
}
if mode == nil || *mode == STR_NORMAL {
// no-op, just to use the constant :)
}
mc.Name = result
}
Nous pouvons voir ici la déclaration de constantes globales, de classes, de constantes de classes et de méthodes de classes. On remarque aussi l’utilisation des méthodes pour le jonglage de types pour les chaînes de caractères ! Il est important de noter que le générateur pourra difficilement couvrir l’intégralité des possibilités offertes par les extensions PHP. Il peut cependant être d’une véritable aide pour un projet d’extension sans fonctionnalités excessivement avancées. Nous vous renvoyons à nouveau vers la documentation qui expose l'intégralité des fonctionnalités supportées par le générateur.
Pourquoi des extensions Go ?
Il est légitime de se demander l’intérêt des extensions écrites en Go pour PHP. Après trois décennies d’existence, c’est une excellente nouvelle de voir que PHP continue de s’intégrer si bien avec des technologies bien plus récentes. L’interfaçage avec Go nécessite FrankenPHP. Depuis la récente annonce de la PHP Foundation qui prend officielllement FrankenPHP sous son aile, son utilisation promet de continuer à croître et sa pérennité est assurée.
L’idée derrière les extensions Go tient en quelques mots : goroutines et wrappers. Premièrement, les goroutines sont un modèle de concurrence haute-performance qui fait, entre autres, la réputation du langage. Avoir la possibilité d’utiliser les goroutines au sein même du code PHP pour des opérations potentiellement lourdes et/ou longues ouvre un grand champ de possibilités. Secondement, les wrappers de bibliothèques existantes ouvrent tout autant de nouveautés. Beaucoup de bibliothèques Go sont reconnues pour leur qualité mais ne sont malheureusement pas disponibles en PHP. On peut citer le système de cache etcd, pour lequel notre coopérateur Kévin a justement créé une extension Go complète afin de permettre son utilisation avec PHP. Vous pouvez retrouver cet exemple dans le dépôt de l’extension. Le fait de proposer un générateur d’extension est une véritable avancée vers la démocratisation de la création d’extensions PHP et permet de baisser le niveau d’entrée requis jusqu’à présent. Tout le monde peut s’y essayer rapidement, explorer le code généré pour comprendre les rouages et pourquoi pas proposer la prochaine extension de référence dans l’écosystème PHP !
Si vous souhaitez en savoir plus en apprendre à écrire vos extensions, la documentation vous expliquera à la fois comment utiliser le générateur, mais aussi comment écrire vos extensions Go sans utiliser le générateur. Nous avons hâte de voir de quelle manière sera utilisée cette symbiose unique entre les deux langages !