IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Voir le flux RSS

Le Blog d'un Ninja codeur

[Actualité] [JS] Interpréter du JavaScript en JavaScript, ce n'est pas si simple ! (Partie 2)

Note : 2 votes pour une moyenne de 3,00.
par , 26/04/2015 à 22h36 (2170 Affichages)
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 - Visualiser dans une fenêtre à part
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 - Visualiser dans une fenêtre à part
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é.

Envoyer le billet « [JS] Interpréter du JavaScript en JavaScript, ce n'est pas si simple ! (Partie 2) » dans le blog Viadeo Envoyer le billet « [JS] Interpréter du JavaScript en JavaScript, ce n'est pas si simple ! (Partie 2) » dans le blog Twitter Envoyer le billet « [JS] Interpréter du JavaScript en JavaScript, ce n'est pas si simple ! (Partie 2) » dans le blog Google Envoyer le billet « [JS] Interpréter du JavaScript en JavaScript, ce n'est pas si simple ! (Partie 2) » dans le blog Facebook Envoyer le billet « [JS] Interpréter du JavaScript en JavaScript, ce n'est pas si simple ! (Partie 2) » dans le blog Digg Envoyer le billet « [JS] Interpréter du JavaScript en JavaScript, ce n'est pas si simple ! (Partie 2) » dans le blog Delicious Envoyer le billet « [JS] Interpréter du JavaScript en JavaScript, ce n'est pas si simple ! (Partie 2) » dans le blog MySpace Envoyer le billet « [JS] Interpréter du JavaScript en JavaScript, ce n'est pas si simple ! (Partie 2) » dans le blog Yahoo

Mis à jour 27/04/2015 à 12h40 par yahiko

Catégories
Jeux vidéos , Javascript , Développement Web

Commentaires

  1. Avatar de phpiste
    • |
    • permalink
    Merci pour cette explication très constructive

    Ahmed.
  2. Avatar de yahiko
    • |
    • permalink
    Il n'y a pas de quoi
  3. Avatar de phpiste
    • |
    • permalink
    à votre avis des sites comme jsbin utilisent du javascript pour interpréter du js ?
  4. Avatar de yahiko
    • |
    • permalink
    Citation Envoyé par phpiste
    à 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).
  5. Avatar de SylvainPV
    • |
    • permalink
    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 - Visualiser dans une fenêtre à part
    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.
  6. Avatar de yahiko
    • |
    • permalink
    Ç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.
  7. Avatar de marts
    • |
    • permalink
    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 - Visualiser dans une fenêtre à part
    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.
  8. Avatar de yahiko
    • |
    • permalink
    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.