Compiler PHP en binaire natif sans interpréteur
Il y a environ quinze ans, Facebook a tenté de résoudre les problèmes de performance de son monolithe avec le compilateur HPHPc, qui convertissait PHP en C++. Plus tard, ils ont abandonné cette approche au profit de la machine virtuelle HHVM, et PHP 8 a introduit un JIT intégré. Il semblait que l'idée d'un compilateur pur Ahead-Of-Time pour PHP était définitivement tombée dans l'histoire. L'équipe Swoole a décidé de revisiter cette idée et a publié le projet TypePHP.
Il s'agit d'un compilateur AOT complet écrit en PHP lui-même. Il traduit le code source PHP 8.4+ en C++17, puis le compile via GCC ou Clang en code machine natif. Pas d'opcodes à l'exécution, pas de préchauffage JIT, et pas d'interpréteur dans le runtime.
Le compilateur est entièrement auto-hébergé. L'utilitaire tpc se construit lui-même à partir des sources PHP, sans aucun code glue C à l'intérieur du compilateur lui-même.
Comment fonctionne TypePHP
La compilation se déroule en deux étapes. D'abord, l'analyseur parse le code du projet, collecte les métadonnées sur les classes et fonctions, et construit une table des symboles complète. Dans la deuxième étape, les corps de fonctions sont traduits en C++17. Dans les chemins critiques, le compilateur génère du C++ statique rapide, tandis que pour les constructions dynamiques comme la réflexion ou les appels aux fonctions système, il se connecte au runtime Zend via la couche PHPX.
La sortie peut être dans l'un des trois formats :
- Binaire exécutable (
bin). Un fichier autonome pour les utilitaires console et les daemons. Nécessite un point d'entréemain(). - Extension PHP (
ext). Un module.soou.dllrégulier qui peut être chargé dansphp.inistandard. - Bibliothèque dynamique (
lib). Un fichier binaire avec les fichiers.stub.phpgénérés pour l'appel depuis d'autres projets. - Composant WASI pour l'exécution dans un environnement WebAssembly.
Ce qui change dans la syntaxe
TypePHP ne cherche pas à prendre en charge toutes les astuces dynamiques de PHP standard. Les créateurs misent sur le typage strict là où la vitesse maximale est nécessaire.
Types scalaires natifs
La directive use native_types indique au compilateur de mapper int, float et bool directement aux types C++ (int64_t, double, bool). Une variable avec un tel type ne peut plus soudainement changer de type au milieu de l'exécution d'une fonction. Mais le processeur exécute de vraies instructions machine sans désempaqueter zval.
<?php
use native_types;
function fib(int $n): int
{
if ($n == 1 || $n == 2) {
return 1;
}
return fib($n - 1) + fib($n - 2);
}
function main(int $argc, array $argv): void
{
$n = (int)$argv[1];
$begin = microtime(true);
echo fib($n) . "\n";
echo "Time: " . (microtime(true) - $begin) . "\n";
}
Il est construit avec une seule commande :
bin/tpc.php fib.php -O3 -o fib
./fib 35
Conteneurs à typage strict
Les tableaux PHP réguliers sont universels mais gourmands en mémoire et lents en raison des tables de hachage. TypePHP ajoute les structures std::vector, std::array, std::map et std::ordered_map.
<?php
use native_types;
function main(): void
{
$vector = std::vector(Type::Int);
$vector[] = 10;
$vector[] = 20;
$vector[] = 30;
$sum = 0;
foreach ($vector as $val) {
$sum += $val;
}
echo "Sum: " . $sum . "\n";
$map = std::ordered_map(Type::String, Type::Int);
$map["alpha"] = 1;
$map["beta"] = 2;
}
Dans les tests des développeurs, une boucle mettant à jour les éléments dans std::array a pris 6,4 secondes contre 67,6 secondes pour un tableau PHP régulier avec JIT activé. La vitesse correspondait essentiellement à un vector C++ écrit à la main (6,2 secondes).
Méthodes sur les primitifs
Au lieu d'une multitude de fonctions comme strlen(), strtoupper() ou in_array(), vous pouvez appeler des méthodes directement sur les types de base. Le compilateur résout ces appels au moment de la construction et les convertit en appels de fonctions C directs sans surcharge de table virtuelle :
<?php
function main(): void
{
$str = "hello world";
echo $str->upper() . "\n";
echo $str->substr(0, 5) . "\n";
$items = [1, 3, 5, 7];
var_dump($items->contains(3));
}
Génération de code template via les attributs
Pour éviter d'écrire des dizaines de getters et setters à la main, TypePHP traite les attributs personnalisés pendant la compilation :
<?php
#[Printer(fields: ['id', 'name'])]
#[Arrayable(fields: ['id', 'name'])]
final class User
{
#[Constructor, Getter, With]
public int $id;
#[Constructor, Getter, Setter]
public string $name = 'guest';
}
function main(): void
{
$user = new User(1);
$user->setName('Ivan');
$copy = $user->withId(2);
echo $user->getId() . "\n"; // 1
echo $copy->getId() . "\n"; // 2
echo $user . "\n"; // User(id=1, name=Ivan)
}
L'attribut #[With] génère une méthode qui clone l'objet, modifie le champ et retourne une nouvelle instance. C'est pratique pour les DTOs immuables.
Intégration directe avec C++
Si un algorithme manque en PHP, vous pouvez l'écrire en C++ et le placer à côté. La liaison se fait via un fichier stub avec un corps vide :
// math.cpp
#include <phpx.h>
using namespace php;
Int php_fast_sum(Int a, Int b) {
return a + b;
}
<?php
// math.stub.php
function fast_sum(int $a, int $b): int {}
<?php
// main.php
function main(): void
{
echo fast_sum(10, 20) . "\n";
}
Performance dans les benchmarks
Selon les mesures des auteurs sur les tests synthétiques standard du dépôt php-src (bench.php et micro_bench.php avec le flag -O3) :
bench.phpse termine en 0,603 s contre 5,034 s pour l'interpréteur standard (environ 8x plus rapide) ;micro_bench.phps'exécute en 2,021 s contre 13,045 s (6,5x plus rapide).
Ces chiffres sont attendus pour la compilation AOT de calculs et de branchements. Dans les vraies applications web, la majeure partie du temps est consacrée aux opérations I/O et de base de données, donc l'amélioration sera plus modeste. Mais pour les tâches de calcul et les workers d'arrière-plan, la différence est perceptible.
Limitations et compromis
Vous ne pouvez pas encore porter un projet Laravel ou Symfony existant vers TypePHP. Il y a un certain nombre de règles strictes :
- Le code exécutable dans la portée globale est interdit — tout le code doit se trouver à l'intérieur de fonctions ou de méthodes.
- PHP 8.4 ou 8.5 avec la bibliothèque
libphp.socompilée (embed SAPI) est requis pour construire un binaire. - Vous avez besoin de GCC 9+ avec support C++17, CMake, et les bibliothèques GMP/MPFR pour les calculs précis.
- Certaines fonctionnalités dynamiques du langage, comme le changement de type libre ou les références complexes, ne sont pas intentionnellement prises en charge.
Le fichier de configuration project.yml aide à gérer les dépendances et les drapeaux d'optimisation dans les grands projets :
name: myapp
mode: bin
php-version: "8.5"
optimize: 2
job: 8
build-dir: build
cxx-std: c++17
sources:
- src
- cpp-src
link-libs:
- curl
Qui bénéficiera de ce projet
TypePHP est en développement actif. Il compte moins de mille étoiles sur GitHub pour l'instant, mais le projet est soutenu par l'équipe expérimentée de Swoole.
Il est pertinent d'essayer le projet si vous avez besoin de :
- Construire un utilitaire CLI léger ou un microservice comme un binaire unique sans avoir besoin d'installer PHP sur le serveur cible.
- Masquer le code source de l'application lors de la livraison à un client on-premise, car les binaires sont significativement plus difficiles à décompiler que le bytecode.
- Écrire une extension PHP avec des calculs lourds sans plonge profondément dans C et l'API Zend.
- Accélérer des modules de calcul isolés, comme des parseurs, des empaqueteurs de données ou des algorithmes de scoring.
Pour commencer, clonez simplement le dépôt, construisez un script de test via bin/tpc.php app.php, et examinez le C++ généré dans le répertoire build.
Projets similaires