5min.

Héberger un projet PHP sans serveur avec WebAssembly

D’après Wikipedia :

WebAssembly, abrégé wasm, est un standard du World Wide Web pour le développement d’applications. Il est conçu pour remplacer JavaScript avec des performances supérieures. Le standard consiste en un bytecode, sa représentation textuelle et un environnement d’exécution dans un bac à sable compatible avec JavaScript. Il peut être exécuté dans un navigateur Web et en dehors. WebAssembly est standardisé dans le cadre du World Wide Web Consortium.

Ce qu’il ne faut pas retenir, c’est que wasm est conçu pour remplacer JavaScript.

Ce qu’il faut retenir, c’est que WebAssembly est un langage de bas niveau qui est conçu pour être exécuté dans un contexte sécurisé. On le retrouve aussi bien dans un navigateur, que dans des workers @edge (comme chez fastly) ou dans les blockchains/smart contracts. Sa force est sa sandbox qui, par défaut ne permet pas d’interagir avec l’extérieur : pas de réseau, pas de système de fichier, etc.

Il existe cependant une interface WebAssembly System Interface (WASI) qui permet de communiquer avec l’extérieur. C’est dans celle-ci qu’il est possible de définir et configurer les fonctions qui permettent d’interagir avec d’autres systèmes.

Wasm est un langage qui est ensuite exécuté par un Runtime Environment. C’est une machine virtuelle qui va interpréter le bytecode wasm. Ce n’est ni plus ni moins qu’un « Processeur basé sur la pile » (Stack Machine en Anglais).

Mais ne vous méprenez pas, nous n’écrivons jamais du wasm a la main. Nous utilisons des langages de plus haut niveau, comme Rust, C, C++, etc. qui sont ensuite compilés en wasm.

Section intitulée notre-projet-en-quelques-motsNotre projet en quelques mots

À JoliCode, nous maintenons une librairie qui corrige automatiquement les problèmes de micro-typographie sur les contenus Web. Nous proposons aussi une demo en ligne. Cependant, cette démo est en PHP. Cela veut dire que nous avons besoin d’un cluster Kubernetes avec 80 nodes d’un serveur pour la faire tourner. Cela implique une dépense d’argent, de la maintenance, un serveur allumé en permanence, etc. Nous aimerions donc la migrer en wasm. Reste à savoir si c’est possible !

Section intitulée le-codeLe code

Le code à faire tourner dans le navigateur est le suivant :

require '/app/vendor/autoload.php';

use JoliTypo\Fixer;

$fixer = new Fixer('fr');
$fixer->setLocale(['Dash', 'Dimension', /*...*/ 'Unit']);

echo $fixer->fix('Merci de fixer ce texte.');

Rien de spécialement compliqué ?! Sauf qu’il y a quand même un require, et donc des fichiers à charger… (les dépendances Composer).

Section intitulée php-et-webassemblyPHP et WebAssembly

PHP étant écrit en C et C++, il est possible de le compiler en wasm. Nous n’allons pas réinventer la roue. Plusieurs personnes ont déjà essayé et ont publié leur travail. Nous allons nous baser sur le travail de soyuka avec php-wasm, merci à lui !

Section intitulée le-buildLe build

L’objectif est de compiler PHP en wasm. Pour se simplifier la vie, Docker est notre ami ! Nous n’allons pas détailler tout le Dockerfile ici, mais les grandes lignes sont les suivantes :

  • Il utilise une image de base emscripten/emsdk. Emscripten qui est un compilateur pour wasm et qui est optimisé pour la compilation de C et C++ (ou autre langage utilisant LLVM). Certains autres langages, comme rust, savent compiler directement en wasm. Ils n’ont donc pas besoin d’emscripten ;
  • Il faut ensuite télécharger PHP, et le compiler avec emscripten en version embarqué (embed) ;
  • Il faut écrire une série de fonctions (source/phpw.c) qui permettent de faire le lien entre le code PHP (en C) et le code wasm. Ces fonctions seront disponibles dans le binaire wasm, et donc exposées en JavaScript ;
  • Et pour finir, il faut compiler le tout avec emscripten.

Notons au passage qu’on active le support de lidbfs, un système de fichier virtuel qui permet de lire ou écrire des fichiers depuis le navigateur. Il nous permettra de charger nos fichiers en PHP.

Une fois la compilation terminée, nous avons besoin d’embarquer nos fichiers PHP. Pour cela, nous utilisons file_packager (un outil fourni par emscripten) qui va créer un fichier php-web.data.js qui contient nos fichiers PHP. Ce fichier sera ensuite chargé dans le navigateur.

Et voilà 🎉

Maintenant que nous avons les fichiers suivants, que faire avec ?

build (20.54 MiB)
├─ php-web.data (13.40 MiB)
├─ php-web.wasm (6.66 MiB)
├─ php-web.data.js (224.91 KiB)
└─ php-web.mjs (270.59 KiB)

Section intitulée integration-en-javascriptIntégration en JavaScript

Pour intégrer notre wasm dans notre application, nous allons utiliser le module php-web.mjs. Ce module est un module ES6, qui est le standard pour les modules JavaScript. Il est donc possible de l’importer dans notre application :

// Importe le point d'entrée du module WASM
import phpBinary from "./build/php-web.mjs";

// Buffer qui contiendra la sortie de PHP
const buffer = [];

// Initialise notre module, et bind la fonction `print()`
// qui permet de récupérer la sortie de PHP
const {ccall, FS} = await phpBinary({
    print(data) {
        buffer.push(data);
    },
})

// Exemple qui affiche la version de PHP
console.log(ccall("phpw_exec", "string", ["string"], ["phpversion();"]));

// Exécute un fichier PHP
ccall("phpw", null, ["string"], ["/app/src/index.php"]);
console.log(buffer.join(''));
buffer.length = 0;

Détaillons un peu ce code nouveau :

  • ccall() est une fonction qui permet d’appeler une fonction wasm depuis JavaScript. Elle prend en paramètre le nom de la fonction, le type de retour, les types des paramètres et les paramètres. C’est une fonction qui est fournie par emscripten ;
  • phpw_exec et phpw sont des fonctions que nous avons défini dans le code C publié dans soyuka/php-wasm. Elles permettent respectivement d’exécuter du code PHP et d’exécuter un fichier PHP.

Section intitulée what-a-time-to-be-aliveWhat a time to be alive!

Et maintenant que nous avons un POC qui tourne dans le browser, nous n’avons plus qu’à tout automatiser avec notre task runner préféré Castor et GitHub Actions pour deployer tout ça sur GitHub Pages.

Les plus curieux pourront aller voir la pull request numéro 100 sur github ou directement la démo.

Section intitulée conclusionConclusion

wasm est vraiment un sujet d’étude très intéressant, et permet de faire beaucoup de choses fun. Nous l’avions déjà utilisé pour exécuter du code rust dans des workers fastly, mais c’est la première fois que nous l’utilisons pour exécuter du PHP dans le navigateur.

Il y a un vrai engouement autour de wasm et il est fort probable que nous en entendions parler de plus en plus dans les années à venir.

Solomon Hykes, un co-fondateur de Docker, a écrit en 2019 :

If WASM+WASI existed in 2008, we wouldn’t have needed to create Docker. WebAssembly on the server is the future of computing.

Et maintenant que faire ? Et bien le système d’exécution du code n’est pas optimal. En effet, on génère le code PHP en JS, pour l’écrire dans le système de fichier virtuel, pour ensuite le lire et l’exécuter. Il serait bien plus intéressant de pouvoir exécuter une fonction JavaScript, bindée à une fonction PHP directement. C’est ce que nous allons essayer de faire dans la prochaine version de ce projet.

Quelques ressources pour aller plus loin :

Commentaires et discussions

Ces clients ont profité de notre expertise