6.17 Une implantation de LispE en WASM - naver/lispe GitHub Wiki

Une implémentation de LispE pour WebAssembly

English Version

Il est désormais possible d'enrichir vos programmes JavaScript avec Lisp. Vous pouvez exécuter des instructions telles que (geometric_distribution 20 0.3) directement dans votre code JS et obtenir un Int32Array contenant le résultat de cette fonction.

Nous proposons (voir ci-dessous) un terrain de jeu pour tester LispE dans votre navigateur préféré. En particulier, index.html vous montre comment intégrer cette bibliothèque dans votre code.

Qu'est-ce que WebAssembly?

JavaScript a certaines limitations, malgré des progrès significatifs, qui le rendent inutilisable pour certaines applications telles que les jeux ou l'apprentissage automatique. Le W3C a donc proposé un nouveau type de code : le WebAssembly pour résoudre ces problèmes. WebAssembly est un format binaire, un bytecode que les navigateurs d'aujourd'hui peuvent compiler et exécuter. Il fonctionne comme une extension de JavaScript et peut approcher les performances des applications natives, bien que selon certaines études, les performances soient généralement de 10 % à 60 % moins rapides que le programme C++ correspondant. Surtout, ce bytecode est maintenant devenu une cible de compilation pour les langages de programmation les plus courants tels que C++ ou Java. Ainsi, il est maintenant possible de prendre un code C++ existant et de le compiler en WebAssembly.

Emscripten : C++ vers WASM (WebAssembly)

Il existe plusieurs solutions pour compiler vers WebAssembly aujourd'hui, et l'une des plus connues est Emscripten. Emscripten est basé sur le compilateur LLVM, mais offre en résultat non seulement une bibliothèque WASM (WebAssembly), mais aussi des fichiers HTML et JS pour l'utiliser.

C'est l'outil que nous avons utilisé pour transformer LispE en une bibliothèque WebAssembly. Notez que nous nous sommes inspirés de Pragmatic compiling of C++ to WebAssembly. A Guide. pour commencer ce projet.

Compilation

Nous avons compilé avec succès une version de LispE en une bibliothèque WebAssembly qui peut fonctionner sur n'importe quel navigateur Web. Cependant, nous avons rencontré des défis lors du portage de LispE vers WebAssembly, notamment en ce qui concerne la gestion des exceptions.

WebAssembly est un format binaire de bas niveau qui vise à fournir une exécution rapide et portable des applications Web. Cependant, WebAssembly n'a pas encore de prise en charge native de la gestion des exceptions. Au lieu de cela, il s'appuie sur JavaScript pour attraper et renvoyer les exceptions à l'aide d'une balise spéciale qui identifie le type et les données de l'exception.

Dans notre première version de la bibliothèque LispE WebAssembly, nous avons utilisé des blocs try/catch pour gérer les erreurs dans notre code. Si cela fonctionne correctement pour une version binaire, ça s'est avéré assez problématique dans WASM. Tout d'abord, c'est très lent car chaque exception doit passer par JavaScript et revenir à WebAssembly. Deuxièmement, ce n'est pas très élégant car chaque appel de fonction est encapsulé dans une fonction JS.

Nous avons écrit un programme LispE pour analyser notre code C++ et remplacer chaque bloc try/catch par un mécanisme plus simple, où une erreur est signalée en allouant un champ spécifique dans la classe principale de l'interpréteur. De cette façon, nous avons préservé notre code d'origine, tout en nous permettant de créer une version beaucoup plus rapide pour WASM.

Le résultat est assez frappant lorsque nous avons testé cette nouvelle version avec certains de nos scripts de test. Par exemple, lorsque nous avons exécuté notre interpréteur sur le script suivant: descente stochastique, cela a pris 9 ms sur un iMac Intel, 153 ms avec la version WASM utilisant try/catch, et 20 ms avec la version WASM sans try/catch.

Sortie

Le résultat de la compilation avec Emscripten consiste en une bibliothèque lispe.wasm et deux fichiers lispe.hmtl et lispe.js. Nous avons implémenté un quatrième fichier lispe_functions.js pour encapsuler les fonctions exportées de la bibliothèque WASM en JavaScript (voir lispe_functions.js).

Le fichier lispe.js, qui est généré par emstdk, ne contient que le code minimal pour charger la bibliothèque et l'initialiser.

Fichiers WASM

Vous pouvez trouver tout le code nécessaire dans le répertoire wasm:

  • lispe.wasm est la bibliothèque WebAssembly qui vous permet d'exécuter du code Lisp.
  • lispe.js gère le chargement et l'initialisation de la bibliothèque WebAssembly.
  • lispe_functions.js fournit les wrappers JS pour accéder aux fonctions exportées par lispe.wasm.
  • index.html montre comment intégrer cette bibliothèque dans une page Web avec une interface de base pour expérimenter avec le langage.

API JavaScript

Cette API a une fonctionnalité clé: elle implémente des méthodes qui renvoient des structures de données réelles qui peuvent être utilisées directement en JS. Ces fonctions peuvent renvoyer des valeurs numériques, des chaînes de caractères ou des tableaux de nombres et de chaînes. Ils peuvent également initialiser des variables dans l'interpréteur. Cela est particulièrement utile si vous voulez passer une grande chaîne ou un grand tableau au code au préalable. La plupart d'entre elles prennent des chaînes de caractères en entrée.

Nous avons également ajouté une fonction supplémentaire pour nettoyer l'interpréteur:

  • function callResetLispe() cette fonction réinitialise l'interpréteur LispE.

Notez que le code et les variables créés dans LispE sont persistants, d'où la nécessité de pouvoir régénérer l'interpréteur lorsque cela est nécessaire. Ainsi, entre deux appels, les variables conservent leur valeur.

Utilisation de la mémoire

Tout d'abord, il est important de noter que WebAssembly fonctionne dans un environnement bac à sable, ce qui garantit que la mémoire ne peut pas être corrompue ou perdue pendant l'exécution du programme. Pour y parvenir, la mémoire disponible doit être définie lors de la compilation de WebAssembly en utilisant emcc. Nous avons compilé LispE avec les valeurs suivantes:

INITIAL_MEMORY=47972352 
STACK_SIZE=20971520

47,972,352 correspond à 732x65536 pages mémoire.
20,971,520 correspond à 320x65536 pages mémoire.

Il est essentiel de comprendre que cette mémoire sert non seulement d'espace d'exécution pour un programme WebAssembly, mais également de zone d'échange pour les fonctions exportées par la bibliothèque. Par conséquent, les arguments des fonctions WASM doivent être alloués dans cet espace mémoire spécifique.

Chaînes de caractères

L'un des aspects les plus difficiles de WebAssembly est qu'il nécessite que les chaînes de caractères soient passées sous forme de tableaux d'entiers, où chaque caractère est encodé en UTF-16. Par conséquent, ces tableaux sont d'abord convertis en chaînes de caractères UTF-32 en LispE, car c'est l'encodage de base des chaînes de caractères dans ce langage, puis, si la sortie est également une chaîne de caractères, ils sont convertis de nouveau en tableaux d'entiers.

Nous fournissons de nombreuses fonctions différentes pour encoder et décoder ces tableaux pour les caractères, les entiers et les flottants.

// Encode une chaîne de caractères en un tableau d'entiers
const encode = function stringToIntegerArray(str, array) { ... };

// Décode un tableau d'entiers en chaîne de caractères
const decode = function integerArrayToString(array, sz) { ... };

// Fournit des caractères sous forme d'entiers
function provideCharactersAsInts(code) { ... };

etc.

Terrain de jeu: index.html

Nous avons fourni un exemple d'utilisation de cette bibliothèque dans le fichier index.html. Vous devez d'abord lancer un serveur.

Lancement d'un serveur

Déplacez-vous dans le répertoire où se trouve lispe.wasm.

Exécutez :

emrun --port 8080 .

Vous pouvez également utiliser Python :

python -m http.server --directory . 8080

Ensuite, vous pouvez utiliser l'adresse : http://localhost:8080 dans n'importe quel navigateur pour interagir avec LispE.

Exécution de code

Comme le montre l'exemple, il est possible d'interagir avec lispe.wasm de manière simple. Vous pouvez ajouter du code, le compiler et l'exécuter instantanément. Pour ré-exécuter du code, positionnez simplement le curseur au début de la ligne que vous souhaitez ré-exécuter.

Par défaut, la méthode callEval prend le code et renvoie une chaîne de caractères en résultat. Dans de nombreux cas, cela peut être adéquat, mais il est également possible de renvoyer des valeurs numériques ou des listes de valeurs numériques. Vous pouvez même minimiser la compilation en initialisant directement des variables avec des listes ou des chaînes de caractères, par exemple (voir callSetq).

En référence à notre exemple précédent : (geometric_distribution 20 0.3), il est possible de l'exécuter de manière à renvoyer un tableau JavaScript ou une chaîne de caractères.

Notez que dans l'interface graphique, le premier cas est couvert par le bouton As a list of integers, tandis qu'un simple appel à Reset/RUN renverra une chaîne de caractères. De plus, la zone de texte : DATA est automatiquement transformée en une variable de nom DATA (sic) dans l'interpréteur, avec comme valeur le contenu de cette zone de texte.

Les fonctions sous-jacentes qui sont appelées sont les suivantes:

var result = callEvalToInts("(geometric_distribution 20 0.3)", 1000), ce qui donne : 8,0,4,1,0,2,2,2,4,1

tandis que

var result = callEval("(geometric_distribution 20 0.3)", 1000), donne : "(2 1 0 1 1 0 0 11 5 11)"

nous oblige à analyser la chaîne ci-dessus pour extraire la table correspondante.

Persistance

Chaque appel à une fonction callEval conserve toutes les variables créées en mémoire. Ainsi, il est possible d'enchaîner les appels sans perdre les valeurs intermédiaires :

(async () => {
    await callEval("(setq r 100)", 1000)
    await callEval("(+= r 1000)", 1000)
    await callEval("(println r)",1000)
    await callEval("r", 100);
})();

Remarquez deux choses sur le code ci-dessus :

  1. Les appels consécutifs sont asynchrones.
  2. La dernière ligne exécutée dans un code renvoie la valeur finale de l'évaluation, dans ce cas r qui vaut 1100.

D'autre part, ces variables n'existent que dans l'espace mémoire de lispe.wasm. Cela garantit la confidentialité des données traitées car l'exécution est locale et ne passe par aucun serveur. Les données sont conservées dans la mémoire locale de l'utilisateur et non dans celle d'un serveur distant.

Bien sûr, il est beaucoup plus efficace d'appeler callEval avec tout le code en une fois. Néanmoins, l'exécution renverra en résultat la dernière ligne du code.

Conclusion

Aujourd'hui, WebAssembly offre aux développeurs JavaScript l'accès à des modules compilés dans d'autres langages tels que C++, Rust ou C#. Grâce à des outils tels que emsdk, passer du code C++ au code WebAssembly est relativement simple, bien que la gestion de la mémoire et le passage des arguments nécessitent une certaine attention. Ainsi, nous pouvons offrir une extension très intéressante à JavaScript sous la forme d'un interpréteur LispE, qui complète JavaScript avec un ensemble très riche d'instructions, en particulier pour la manipulation de listes et de chaînes de caractères, grâce à une intégration serrée.

En particulier, LispE offre la possibilité d'écrire des codes en une ligne remarquablement compacts :

  • Séparer une chaîne en sous-chaînes
// Sépare une chaîne en sous-chaînes
(segment "This is the 100 test"); ("This" "is" "the" "100" "test")
  • Filtres puissants basés sur une expression régulière :
(filterlist (λ(x) (rgx_match (rgx "%C%a+") x)) (segment "The lady lives in Paris. She lives in Montmartre")

qui donne : ("The" "Paris" "She" "Montmartre")

L'API permet d'exploiter le langage en retournant des listes ou des valeurs directement. Ainsi, un appel à l'exemple de filtre renverra un tableau de chaînes de caractères :

lst = callEvalToStrings("(filterlist (λ(x) (rgx_match (rgx "%C%a+") x)) (segment "The lady lives in Paris")), 1000);

// lst est : La,Paris

Enfin, l'exécution locale du code garantit la confidentialité des données de l'utilisateur et facilite les vérifications ou les calculs dans le navigateur sans avoir à passer par un serveur.

LispE est un langage très riche comme le montre la documentation qui offre aux développeurs d'autres façons de concevoir et de mettre en œuvre leur code pour le Web.