Plongee dans JsInterop

La genèse de la phase 2...

Arnaud Tournier, le 27-01-2015

Plongée dans JsInterop

Présenté par Ray Cromwell, de l’équipe Google Gwt et Inbox.

Cette session porte sur l’amélioration de l’intéropérabilité entre Java et Javascript et sur la genèse du nouveau mécanisme JsInterop. Elle fera aussi office de pré-requis pour la session sur Inbox.

En tant que developpeur Java, pourquoi devrait-je connaître le JsInterop ?

En 2006, utiliser l’intéropérabilité entre les différents écosystèmes Javascript était rare. Mais personne ne s’attendait à avoir de grosses applications en Javascript et donc à avoir à intégrer des outils très variés dans une seule application.

Aujourd’hui en 2014, la communauté Javascript est bien plus riche et active. On peut vouloir trouver un bénéfice de cette communauté. Aujourd’hui si vous souhaitez intégrer une bonne bibliothèque Javascript, le choix que vous avez se réduit plus ou moins à soit la wrapper avec JSNI (ce qui est long et fastidieux) soit à la réécrire entièrement en Java…

JsInterop

But : utiliser toute api Javascript en Java. Mais aussi exporter du code Java vers le monde JavaScript.

Démo : Ray nous montre la facilité avec laquelle il wrappe JQuery

    @JsType // type Javascript
    interface JQuery {
        JQuery css(String prop, String val); // api fluent !
        JQuery click( Consumer<Event> e );
    }

Nouvelle fonction :

    // C'est du Java n'oubliez pas ;)
    JQuery j = js( "$('ul > li')" );

    j.css("color", "red")...

JsType

Permet d’exporter n’importe quel type Java en Javascript.

Exemple

    @JsType
    interface Foo {
        void myMethod();
    }

    class FooImpl implements Foo {
    }

    js("someJsMethod($0)", new FooImpl());


    function someJsMethod(foo) {
        alert( foo.myMethod() );
    }

Le compilateur traite cette annotation et ne change pas le nom des méthodes des classes l’implémentant. Toutefois, le développeur bénéficie quand même des optimisations et le code correspondant à ces interfaces sera éliminé à la compilation s’il n’est pas utilisé par vos programmes.

Un défaut tout de même, ceci ne permet d’exporter ni les méthodes ni les champs statiques.

@JsProperty

Permet de faire passer un appel de méthode Java en accesseur de valeur Javascript.

@JsExport / @JsNamespace

    @JsNamespace("mylib")
    class Foo {
        @JsExport
        Foo(int x) {...}

        @JsExport
        public static final int CONSTANT = 42;
    }

Avec ceci, le compilateur n’élimine pas ces types et les rend accessibles au Javascript par l’intermédiaire du namespace. Dans notre cas d’exemple : en javascript var f = new mylib.Foo() et mylib.Foo.CONSTANT

Autre subtilité :

    class Foo {
        @JsNamespace("mylib")
        @JsExport
        public static final int CONSTANT = 42;
    }

    @JsNamespace("mylib")
    package fr.lteconsulting....;

@JsExport peut aussi être positionné sur la classe entière pour exporter tous ses champs.

Conversion Lambda

On doit souvent passer des fonctions aux API javascript (des callbacks donc).

    // Déclare une single method interface éligible pour les lambdas
    @JsFunction
    interface Callback {
        String result();
    }

    @JsType
    interface EventTarget {
        addEventListener( Callback cb )
    }


    // en Javascript
    x.addEventListener("click", toJsFunction(cb, "result");

Intégrité référentielle

Il faut que l’identité de la fonction Javascript générée automatiquement soit préservée tout au long de l’exécution. Par exemple, pour retirer un eventListener, il faut repasser la fonction qui a été passée au départ.

Tout ceci pris en compte génère néanmoins une limitation : pour être éligible à la conversion automatique en lambda, une classe ne doit pas implémenter plus d’une interface annotée par @JsFunction.

    @JsType(prototype="somePolymer.ctorFunction")
    interface PolymerButton {
        void someMethod();
    }

    class MyPolymerButton extends PolymerButton_Prototype {
        void someMethod() {
            ... my implementation ...
            super.someMethod(); // On peut même appeler la classe mère
        }
    }

Et voila pour la phase 1 !

Que nous manque-t-il alors à inclure dans la phase 2 ?

  • Pas de surcharge des méthode (existe pas en Javascript),
  • Pas de varargs,
  • Pas de moyen propre pour invoquer le constructeurs Javascript natifs,
  • Pas possible d’aligner les collections JRE avec les native javascript array,
  • Pas possible de définir facilement des objets littéraux,
  • Pas de gestion des types boxés.

Inbox

Dans Inbox, JsInterop est exploité au maximum. 70% du code “logique” Java doit parler à 30% du code UI en Javascript.

Pour ceci :

  • La vérification de types à la compilation est poussée jusqu’à vérifier les utilisations illégales par le code Javascript des API Java définies en GWT ! Ceci prévient la mauvaise utilisation en Javascript d’une méthode Java transpilée en Javascript.
  • Approche identique à celle de Google SpreadSheets, c’est à dire l’utilisation de J2ObjC pour la plateforme iOS.

Nouveaux cas d’utilisation

  • Clients full web
  • Applications natives

La future phase 2 de JsInterop (prévue pour GWT 3.0)

Cette phase rendra impérative la présence de Java 8.

Une syntaxe concise pour intégrer des tous petits bouts de javascript :

    // Voilà comment exécuter du Javascript et récupérer la valeur de retour
    String value = js( "$'input#credit-card').value()" );

Ce qui est bien, c’est que c’est facile à écrire. Et en plus le compilateur vérifiera la syntaxe et pourra aussi optimiser…

    JsArray x = ints(3, 5, 6, 4);

Syntaxe pour les varargs

Prototype des varargs avec GWT 3.0

L’overloading sera supporté (exemple du 2dCanvas)

    @JsType
    class Int16Array
    {
        @JsConstructor
        Int16Array newInstance(int x); // manque native !!!
    }

    @JsType(prototype="Object")
    class JsObject {
        public static native String[] keys(Object obj);
    }

Idées pour les objets litteraux

La spécification n’est pas encore décidée mais plusieurs idées se précisent. Ray montre trois étapes dans la réflexion de l’équipe pour terminer celle qui pour l’instant leur plaît le plus, avec un esprit de duck typing:

    @JsType(literal=true)
    class MapOptions {
        ...
    }

Ray montre la syntaxe la plus probable pour les objets littéraux

Pour l’instant c’est la meilleure idée qui rôde dans l’équipe.

Gérer les déviances idomatiques

Java et Javascript sont différents !!! D’où la difficulté de formaliser en Java l’intéropérabilité avec Javascript.

Les développeurs Java renvoient souvent des List<T>, Map<T>. Mais les développeurs Javascript ne sont pas habitués à cela, et c’est normal !

Les @JsConvert sont là pour remédier à ce problème. Ils permettent au programmeur Java de définir l’implémentation utilisée pour convertir les données quand elles passent d’un monde à l’autre. Reste le problème de l’intégrité référentielle pour ce genre de données, il faudra gérer à part l’opérateur equal.

Elemental 2.0

Les API des navigateurs évoluent très rapidement. Elemental a été réécrit dans sa version 2.0. Cet outil permet de programmer avec les dernières fonctionalités des navigateurs puisqu’il va fouiller dans les spécifications du W3C pour trouver les API Javascript et les formaliser en Java avec JsInterop. Il va vraiment également analyser la documentation du W3C et l’intégrer dans la JavaDoc de l’API générée.

Ray nous fait lance la génération d’Elemental en live, car en effet chacun est censé pouvoir le lancer à sa convenance selon ses besoins. On voit l’automate chercher les informations sur w3c.org et ensuite, il nous montre le code généré (avec sa documentation), 100% en JsInterop, prêt à être utilisé. Tout ceci avec zéro overhead à l’exécution !!!

Et voici un aperçu zommé de la capture d’écran :

A noter : Ray a déjà préparé une version d’Elemental qui va chercher ses données dans la base de typage open source destinée à TypeScript. Cette base de donnée fournit les informations de typage de la plupart des bibliothèques Javascript du marché. D’un coup d’un seul, la plus grande partie des bibliothèques Javascript existantes sont de-facto disponibles (et correctement typées) pour Gwt ! Argument de poids.


Arnaud Tournier, le 27-01-2015