
Construire une extension PHP en Rust ou Zig : performance, pièges et possibilités
7 juillet 2025
Mon parcours pour une extension PHP en Rust et Zig : de la lutte contre la surcharge FFI à la découverte de quand les langages modernes gagnent (et quand non). Plongée dans performance, build systems et l’avenir de PHP.
À la poursuite de la memory safety (sans perdre la tête)
Si vous avez déjà écrit une extension PHP en C, vous connaissez la douleur : problèmes mémoire, nettoyage manuel, et cette peur constante du segmentation fault. J’y étais. Trop souvent.
Je voulais m’en sortir.
Alors je me suis demandé : « Puis-je écrire une extension PHP dans un langage moderne et memory-safe comme Rust ou Zig ? »
Spoiler : ce n’était pas aussi direct que je pensais — mais c’était un voyage intense et instructif.
Première étape : Rust (alias le détour unsafe)
Rust semblait le choix évident. Sûr, rapide, populaire dans le monde systems. Je pensais : « J’utiliserai serde pour un parser JSON rapide en Rust, un peu de glue FFI, et c’est réglé. »
Ha.
Une fois dans le FFI, Rust vous lance du unsafe comme des confettis. Soudain j’étais en train de :
- Gérer les lifetimes manuellement
- Me battre avec
bindgen - Coller
#[no_mangle]partout - Voir les perfs chuter à cause de la surcharge Rust→C→PHP
Comme construire un vaisseau spatial moderne… qui doit s’accoster à une calèche.
J’ai abandonné Rust.
Entrée Zig : un souffle d’air (bas niveau) frais
Zig m’a attiré par sa syntaxe simple, la super interop C, et la cross-compilation intégrée. Prévisible et performant, sans la complexité que Rust impose avec les API C.
J’étais partant.
✅ Zig compile les headers C out of the box
✅ Contrôle mémoire manuel (sans les arêtes vives de C)
✅ Pas de cargo, pas de bindgen, pas de drame
Phase 1 : écrire le cœur en Zig
J’ai commencé petit : structs hardcodées pour Person et Company, et une fonction pour les encoder en JSON.
const Person = struct { name: []const u8, age: u32 };
const Company = struct { name: []const u8, employees: []Person };
pub fn encodePerson(p: Person) ![]const u8 { ... }
Mais ça ne suffisait pas pour le monde réel. Je l’ai transformé en convertisseur JSON plus flexible et réutilisable :
const JsonConverter = struct {
options: JsonOptions,
pub fn encode(value: anytype) ![]const u8 { ... }
pub fn decode(comptime T: type, json_str: []const u8) !T { ... }
};
Cool. Il ne restait plus qu’à brancher ça à PHP…
Phase 2 : cauchemars de build system
Les extensions PHP sont des bibliothèques partagées — .so sous Linux, .dll sous Windows. Le build.zig de Zig est génial, mais PHP vit encore dans le monde autotools (salut config.m4, vieil ennemi).
Les choses se sont salies :
- Ma machine dev x86 devait builder pour ARM.
- Zig n’exposait pas
main(), le linker hurlait. - PHP attendait des fichiers
.aà des emplacements précis.
J’ai fini par un build hybride : Zig pour la logique cœur, le build PHP pour la glue. Pas élégant, mais ça marchait.
Phase 3 : écrire la vraie extension PHP
Pour exposer le code Zig à PHP, il fallait le classique zend_module_entry, des wrappers PHP_FUNCTION(), et de la glue pour le parsing des paramètres.
PHP_FUNCTION(fast_json_encode) {
char *input;
size_t input_len;
if (zend_parse_parameters(ZEND_NUM_ARGS(), "s", &input, &input_len) == FAILURE) {
RETURN_NULL();
}
const char *result = zig_json_encode(input, input_len);
RETURN_STRING(result);
}
Simple. Pour que ça paraisse natif, j’ai aussi :
- Ajouté une
FastJsonExceptionpour la gestion d’erreurs - Supporté
JSON_THROW_ON_ERROR - Vérifié que le nettoyage mémoire marchait côté Zig et PHP
La vérité choquante : les perfs étaient… toujours pires ?
La partie qui m’a complètement humilié.
J’ai benchmarké ma nouvelle extension contre json_encode() natif de PHP, en m’attendant à une victoire écrasante pour Zig.
Mais non. Mon extension était toujours plus lente. 😬
Petites payloads ? Plus lent.
Gros documents JSON (10MB+) ? Toujours plus lent.
Batch de 1 000+ appels ? Oui, plus lent aussi.
Pourquoi ? Chaque appel traversait trois frontières :
- PHP vers C (zend_parse_parameters)
- C vers Zig (surcharge FFI)
- Zig retour vers C, puis PHP
La surcharge était si importante que même la perf brute supérieure de Zig ne pouvait pas la compenser.
Le dur retour à la réalité
❌ Petites payloads : plus lent (comme prévu)
❌ Gros JSON (10MB+) : toujours plus lent à cause du FFI
❌ Batch (1 000+ petites payloads) : la surcharge multipliait la douleur
❌ Tout le reste : vous l’avez deviné, plus lent
La leçon ? Parfois les gains théoriques d’un meilleur langage sont entièrement mangés par la surcharge architecturale.
Ce que j’ai appris (beaucoup)
-
Les extensions ne sont pas des baguettes magiques.
Parfois le PHP pur est plus rapide pour les petites tâches. -
Les build systems comptent.
La cross-compilation Zig était géniale, mais les outils build vieux style de PHP étaient un gros point de friction. -
Les API doivent sembler natives.
Imiter le comportement dejson_encode()(erreurs, flags, types de retour) rendait l’extension utilisable. -
Peut-être qu’on pense trop petit.
Au lieu de construire des extensions, et si PHP tournait sur Zig ?- Pas de pénalité FFI
- Memory safety dans le runtime
- Vraies améliorations de perf au niveau VM
Réflexions finales : ça valait le coup ?
Absolument. Pas parce que j’ai construit l’encodeur JSON le plus rapide, mais parce que j’ai pu :
🔹 En apprendre plus sur les internals PHP
🔹 Plonger dans Zig et son build system
🔹 Repenser l’approche de la performance en PHP
Peut-être que l’avenir n’est pas d’écrire des extensions PHP plus sûres.
Peut-être qu’il s’agit de réimaginer PHP lui-même dans des langages plus sûrs et plus rapides.
Qu’en pensez-vous ? Avez-vous déjà essayé une extension PHP en Rust, Zig, ou autre que C ? J’aimerais entendre vos histoires — succès ou horreurs bienvenues !
Vous pouvez voir le projet ici github



