Interpréter du JavaScript en JavaScript
Ce n'est pas si simple : partie 2, par yahiko

Le , par yahiko, Rédacteur/Modérateur
Lors de mon précédent billet, j'avais présenté la fonction eval() et montré en quoi il était peu recommandé de l’utiliser.

Je vais montrer dans ce billet un moyen d’interpréter du code JavaScript sans utiliser eval(), mais avant cela, essayons de voir les cas d'utilisation où l'interprétation de scripts peut s'avérer nécessaire.

Faire appel à des scripts au sein d'une application n'est pas nécessairement un besoin très courant dans la mise en œuvre d'un site web ou même de la plupart des applications que l'on peut rencontrer sur la toile.

Cependant, cela peut s'avérer utile voire nécessaire lorsqu'interviennent des règles pouvant être ajoutées (ou retirées) à postériori de la phase de développement, où lorsque tout simplement il est plus simple ou plus rapide d'implémenter ces règles à travers des scripts plutôt qu'en travaillant sur l'application en elle-même.

Le cas le plus typique et le plus parlant est probablement le jeu vidéo, où il est très fréquent de faire appel à du scripting pour gérer soit des règles très spécifiques et pointues comme l'intelligence artificielle qui font souvent appel à un/des développeur/s dédié/s, ou pour faciliter les développements en fournissant aux développeurs un moyen plus simple d'étendre les fonctionnalités du jeu sans avoir à modifier (et donc à re-tester) le cœur du programme.

Les jeux développés en langage C ou C++ font assez régulièrement appel au langage Lua pour le scripting, plus pour des raisons d'habitudes que pour les qualités intrinsèques de Lua disons-le. Mais avec l'avènement du HTML5 et des jeux online, le choix d'utiliser JavaScript comme langage de scripting, d'autant plus qu'il devient de plus en plus populaire côté serveur avec node.js, est tout à fait envisageable, à condition de pouvoir interpréter du code JavaScript de façon fiable, donc sans faire appel à la primitive eval().

On cherche en fait à pouvoir interpréter du code JavaScript dans une sorte d'environnement protégé, on parle souvent de sandboxing (bac à sable) à ce propos.

Pour cela, le seul moyen n'est ni plus ni moins d'employer un interpréteur JavaScript externe qui nous permettra de lancer des scripts qui n'auront pas accès aux variables globales du programme appelant.

On ne veut donc surtout pas ici que l'interprétation se comporte comme une fermeture (closure), comme c'est le cas de la primitive eval() en mode strict comme vu dans la partie précédente.

Il existe assez peu d'interpréteurs JavaScript implémentés eux-même en JavaScript. Il faut croire que la technologie HTML5 doit encore faire ses preuves dans le domaine vidéo-ludique, mais c'est un autre sujet.

On peut trouver actuellement trois interpréteurs JavaScript open source écrits en JavaScript :


L'interpréteur le plus simple à appréhender des trois est le premier, JS Interpreter, réalisé par un développeur de chez Google, bien que le projet ne soit pas officiellement un projet Google.

Son utilisation est relativement directe comme le montre l'exemple ci-dessous :

Code : Sélectionner tout
1
2
3
4
var myCode = 'var a=1; for(var i=0;i<4;i++){a*=i;} a;';
var myInterpreter = new Interpreter(myCode);
myInterpreter.run();
var result = myInterpreter.value;
Le code JavaScript interprété est ainsi totalement isolé du programme appelant et ne peut pas par exemple modifier des variables globales du programme appelant contrairement à eval().

Cependant, il peut être utile et même nécessaire de rendre accessible, d'exposer, certaines variables ou fonctions du programme appelant au script interprété.

JS Interpreter le permet à l'aide d'un paramètre optionnel, une fonction d'initialisation du contexte, à passer au constructeur de l'interpréteur.

Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
function square(x) {
    return x * x;
}

var initFunc = function(interpreter, scope) {
    var wrapper = function(x) {
        return interpreter.createPrimitive(square(x));
    };
    interpreter.setProperty(scope, 'square', interpreter.createNativeFunction(wrapper));
};

var myInterpreter = new Interpreter('var y = square(5)', initFunc);
Ainsi, nous avons à la fois un environnement JavaScript isolé du programme appelant, tout en ayant la possibilité d'exposer explicitement certaines fonctions, square() dans l'exemple ci-dessus, du programme appelant dans le script.

Nous pouvons donc définir dans le programme appelant un ensemble de fonctions accessible aux scripts formant de la sorte ce qui est commun d'appeler une API de scripting.

A noter tout de même, en bémol, que l'interpréteur JS Interpreter, en l'état, est très basique. Il vous faudra sans doute le modifier et l'adapter pour qu'il puisse être utilisé dans le cadre d'un véritable environnement de scripting, mais c'est déjà un bon début.

Les deux autres interpréteurs JavaScript mentionnés plus haut manquent quand à eux d'une documentation claire et complète pour en saisir facilement toutes les fonctionnalités. Ce qui est assez dommage.

Enfin, il est à regretter que ces trois projets ne semblent plus être réellement maintenus au moment où j'écris ces lignes.

A l'heure où le développement de jeux Web utilisant la technologie HTML5 devient de plus en plus répandu, il reste surprenant qu'il y ait un tel déficit au niveau des interpréteurs JavaScript écrits en JavaScript.

Il pourrait être judicieux par conséquent, c'est une réflexion en cours pour ma part, de forker le projet JS Interpreter pour développer ses fonctionnalités, avec notamment le support de la norme ECMAScript 5 à minima voire d'ECMAScript 6 idéalement.

A ceux que ce projet pourrait intéresser, qu'ils n'hésitent pas à me contacter en message privé.


Vous avez aimé cette actualité ? Alors partagez-la avec vos amis en cliquant sur les boutons ci-dessous :


 Poster un commentaire

Avatar de phpiste phpiste - Membre averti https://www.developpez.com
le 27/04/2015 à 12:30
Merci pour cette explication très constructive

Ahmed.
Avatar de yahiko yahiko - Rédacteur/Modérateur https://www.developpez.com
le 27/04/2015 à 15:15
Il n'y a pas de quoi
Avatar de phpiste phpiste - Membre averti https://www.developpez.com
le 27/04/2015 à 15:21
à votre avis des sites comme jsbin utilisent du javascript pour interpréter du js ?
Avatar de yahiko yahiko - Rédacteur/Modérateur https://www.developpez.com
le 27/04/2015 à 16:53
Citation Envoyé par phpiste Voir le message
à votre avis des sites comme jsbin utilisent du javascript pour interpréter du js ?
Même si je ne connais pas les technologies utilisées en back-office de ces sites, cela m'étonnerait qu'ils fassent appel à un interpréteur JS écrit en JS, ne serait-ce que pour des questions de performance. Ils doivent faire tourner peut-être des milliers de requêtes en simultanées. Peu de chance donc que ce soit du JS.

Je pense plutôt qu'il s'agit d'un moteur en C ou C++ utilisant le moteur JS de Google (V8) ou celui de Mozilla Firefox (SpiderMonkey).
Avatar de SylvainPV SylvainPV - Rédacteur/Modérateur https://www.developpez.com
le 11/05/2015 à 14:42
Charger tout un interpréteur me paraît quand même assez bourrin comme méthode d'isolation du code. N'y aurait-il pas une alternative plus simple, comme utiliser un Web worker par exemple ? Les Web workers ont leur propre scope global, et on peut contrôler les interactions avec le scope global de la page via postMessage :

Code : Sélectionner tout
1
2
3
4
5
myWorker.onmessage = function(e) {
  if(e.data[0] === "claim_global" && EXPOSED_GLOBALS.contains(e.data[1])){
     myWorker.postMessage(["give_global", window[e.data[1]]]);
  }
}
C'est une simple idée qui me vient, je n'ai pas testé la faisabilité de la chose.
Avatar de yahiko yahiko - Rédacteur/Modérateur https://www.developpez.com
le 11/05/2015 à 17:22
Ça dépend de la finalité.
Sur le principe oui, un Web Worker peut exécuter du code via eval() de façon isolée.
C'est de ce point de vue une excellente remarque et peut dépanner si on n'a pas besoin de "binding" avec des fonctions du code parent.
Dans le cas contraire, les Web Workers pourraient se révéler du coup trop isolés.
Car l'échange de données via messages peut se révéler rédhibitoire (en terme de développement voire aussi en terme de perfs, mais là faut voir) que de passer par un interpréteur intégré.

Donc pour un besoin très basique, les Web Workers peuvent se révéler utiles s'il s'agit juste d'exécuter un script JS sans dépendance (ou très peu) avec le code principal, et dont les retraitements du résultat sont minimes.

S'il s'agit de mettre en place une API de scripting, là, la nécessité d'un interpréteur intégré est il me semble incontournable.
Avatar de marts marts - Membre averti https://www.developpez.com
le 02/09/2015 à 14:26
Je suis un peu long à la détente sur ce billet, mais voici une solution que j'ai mise au point ...

Il est possible d'exécuter du code dans le contexte d'un objet, grâce à la structure with, et d'intercepter toutes les affectations au niveau de cet objet, grâce à un proxy.

Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
code = 'a = 2;';
a = 1;
test = function()
{
    var o = {};
    var handler = {
        has : function() {return true;}
    };
    var p = new Proxy(o, handler);
    eval('with (p) {' + code + '}');
    alert(o.a); // => 2
}
test();
alert(a); // => 1
L'objet o constitue en quelque sorte l'objet global dans lequel le code est évalué. Si la variable à assigner n'existe pas, c'est là qu'elle est créée.
Sans l'utilisation du proxy, l'affectation remonterait au contexte parent, c'est à dire l'intérieur de la fonction, puis au contexte global.

Noter que le code évalué aura accès à tout ce qu'on aura préalablement placé dans l'objet.
Avatar de yahiko yahiko - Rédacteur/Modérateur https://www.developpez.com
le 09/10/2015 à 16:32
Désolé aussi pour le retour tardif, je viens de voir votre message aujourd'hui.
En effet, c'est assez ingénieux et pourrait permettre d'utiliser eval de façon plus sécurisée.
Contacter le responsable de la rubrique JavaScript