PHPRustZigPerformanceExtensionsSystems ProgrammingMemory Safety

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 FastJsonException pour 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 :

  1. PHP vers C (zend_parse_parameters)
  2. C vers Zig (surcharge FFI)
  3. 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)

  1. Les extensions ne sont pas des baguettes magiques.
    Parfois le PHP pur est plus rapide pour les petites tâches.

  2. 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.

  3. Les API doivent sembler natives.
    Imiter le comportement de json_encode() (erreurs, flags, types de retour) rendait l’extension utilisable.

  4. 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

Tous les articles
Liens

À lire aussi

juil. 2025

Pourquoi j’ai créé FastCSV-C : simplifier le CSV entre langages

Pendant des années, le CSV en Python et PHP m’a causé des cauchemars — sur de nombreux projets réels. Voici pourquoi j’ai créé FastCSV-C : une bibliothèque CSV rapide et memory-safe en C, intégrable dans n’importe quel langage. Leçons sur FFI, mémoire et performance.

mars 2025

Laravel devient-il trop complexe ? Impact sur les développeurs et les entreprises

Laravel a beaucoup évolué, avec des outils comme Livewire, Inertia.js et Filament. Ces innovations apportent de la valeur, mais soulèvent aussi des questions sur la complexité, la maintenabilité et la stabilité à long terme. Cet article explore l’impact sur les juniors, les seniors et les dirigeants.