Horreurs, Javascript et LLVM

11/2011

Javascript, langage a l'origine incertaine, semble pourtant être en passe de devenir "l'assembleur du web". Par exemple, le toujours prolifique Bellard a, pour le fun, démontré qu'un navigateur moderne était capable de faire tourner un système d'exploitation entièrement en Javascript.

En parallèle, HTML5 avec ses objets canvas pour l'image, ainsi que video et audio les bien nommés, le tout piloté par Javascript, permettent d'envisager des applications multimédia toujours plus avancées sur le web.

emscripten est un projet qui permet de générer du javascript à partir du langage intermédiaire de LLVM. LLVM étant largement compatible avec le C/C++, via CLang, le portage d'applications C++ sur le web devient envisageable.

Le principe de base est de considérer la mémoire de la cible sous forme d'un énorme tableau nommé "HEAP" en Javascript et de transformer les opérations mémoire bas niveau en opérations d'affectation du tableau HEAP global.

Hmmm ?

Pourrait-on alors, pour le fun, utiliser emscripten pour porter des jeux "sur le web" ? La réponse n'est pas aussi affirmative qu'il ne pourrait y paraître.

J'ai pris comme exemple de micro-jeu, un tutoriel sur SDL écrit par Marius Andra. Celui-ci démontre l'utilisation de SDL pour charger des images qui serviront de sprites pour un jeu et comment les animer.

La tâche semblait aisée puisqu'on trouve facilement mention de SDL dans le code de emscripten. Ce dernier implémente en effet "nativement" certaines fonctions (fonctions systèmes open(), read(), etc.) et notamment une sous-partie de SDL.

Cependant l'implémentation actuelle, en plus d'être incomplète, souffre d'un problème de taille : Javascript est "piloté par événements" (event-driven) bien que parfaitement synchrone. Ceci signifie que le langage est prévu pour supporter des gestionnaires qui répondront à des événements "extérieurs" (fin de chargement d'une ressource, fin d'un timer, etc.), mais qu'un script ne peut pas attendre activement qu'un événement arrive.

Par exemple, le chargement d'une image HTML est différée de l'exécution du script. Lorsqu'un script demande la chargement d'une image, celui-ci n'est effectué que quand le navigateur "a le temps". En réalité, ce chargement ne peut arriver que quand le script a rendu la main au navigateur.

L'approche naïve qui consisterait à attendre dans un script que l'image soit chargée ne fonctionne pas :

var loaded = false;
var img = new Image();
// install completion handler
img.onload = function() { loaded = true }
img.src = "image.png";

// busy-wait
while ( !loaded );

alert("Image loaded");

En effet, ici le code exécuté après chargement de l'image (onload()) ne le sera que quand le script aura rendu la main au navigateur et que celui-ci aura daigné charger l'image. Le précédent script fait donc rentrer le navigateur dans une boucle sans fin.

Par ailleurs, il n'existe visiblement aucun moyen d'avoir accès à la boucle de traitement des événements du navigateur. On aurait aimé quelque chose comme :

...
// busy-wait
while ( !loaded )
    processBrowserEvents();

alert("Image loaded");

C'est généralement ce que l'on trouve dans les frameworks orientés événements, quand on a besoin de finesse dans le traitement des événements (par exemple QCoreApplication::processEvents en QT). Et visiblement je ne suis pas le seul à me poser ce genre de questions.

La solution classique consiste à "spaghettiser" le code :

var loaded = false;
var img = new Image();
// install completion handler
img.onload = function() {
    alert("Image loaded");
    var img2 = new Image();
    img2.onload = function() {
        alert("Image2 loaded");
    }
    img2.src = "image2.png";
}
img.src = "image.png";
// do nothing ... get control back to the browser

C'est une horrible manière d'envisager le code : il est parfaitement illisible et il devient impossible de l'encapsuler facilement dans une fonction qu'on aurait par exemple appelée "load_images()" dont on ne veut surtout pas savoir comment elle fonctionne.

Certains semblent indiquer que ce n'est pas vraiment un problème et que vouloir faire des attentes actives dans un script casse le principe (voire ... la beauté) de Javascript.

Certes, mais dans le cas d'un code porté depuis un langage différent, puisque c'est ce qui nous intéresse ici, la structure "linéaire" du code est difficile à modifier a posteriori

.

Extensions du langage

Il existe plusieurs concepts qui pourraient nous aider ici :

Les quatre premiers sont des extensions du langage. Ils nécessitent alors une autre étape de traduction Javascript étendu vers Javascript. Cette étape fait appel à des traducteurs écrits dans des langages divers et variés (dont Javascript lui-même généralement ou encore Common Lisp).

Il faut alors que le traducteur accepte parfaitement toutes les subtilités du Javascript utilisé. Ce qui n'est pas toujours le cas. Par exemple jwacs (écrit donc en Common Lisp) retourne d'obscurs erreurs qui montrent qu'il ne supporte pas complètement le code Javascript généré par emscripten.

StratifiedJS semble répondre à des besoins plus généraux que de simples attentes actives et, d'après ma compréhension, met en branle des mécanismes assez complexes, ce qui fait que les performances ne sont peut être pas au rendez-vous.

Narrative est beaucoup plus simple, mais visiblement n'apporte pas beaucoup plus que les "generators" qui sont une construction native du langage (dans sa version 1.7 certes).

Trampoline de générateurs

Un brillant article montre comment utiliser les générateurs pour mettre en oeuvre le concept de continuation avec une profondeur de pile d'appels quelconque.

Ce serait, je pense, le mieux à faire pour pouvoir utiliser des fonctions "bloquantes" dans le code généré par emscripten. Après une première analyse, ceci nécessite quand même quelques réflexions. En effet, une fonction "bloquante" est une fonction qui rend la main au navigateur via l'utilisation du mot clé yield (ce qui en fait un générateur). C'est aussi ce qui permet au code de reprendre là où il avait été interrompu.

La fonction principale contient une petite boucle, qui est équivalente à une boucle d'interprétation et qui permet de gérer les suspensions et reprises de code, basés sur la récupération de valeurs retournées par les générateurs.

Là où on trouvait une fonction avec des return, il faudra maintenant écrire des yield. L'autre règle est que toute fonction qui appelle une fonction susceptible de bloquer, i.e. un générateur, devra à son tour utiliser yield, et donc devenir un générateur.

C'est donc ce mécanisme qu'il faudrait inclure à la génération de code d'emscripten. A réfléchir ...