Compilation anticipée et génération de classes
Clojure compile à la volée tout le code chargé en bytecode JVM, mais il est parfois avantageux de compiler à l'avance (AOT). Voici quelques raisons d'utiliser la compilation AOT :
- Distribuer votre application sans le code source
- Accroître le démarrage de l'application
- Générer des classes nommées utilisables par Java
- Créer une application qui ne nécessite ni génération de bytecode à l'exécution ni chargeurs de classes personnalisés
Le modèle de compilation de Clojure préserve autant que possible la nature dynamique de Clojure, malgré les limitations de rechargement de code de Java :
- Le chemin d'accès aux fichiers sources et aux classes suit les conventions du classpath Java.
- La cible de la compilation est un espace de noms.
- Chaque fichier, fonction et classe générée produit un fichier `.class`.
- Chaque fichier génère une classe de chargement du même nom, à laquelle est ajouté `__init`.
- L'initialiseur statique d'une classe de chargement produit le même effet que le chargement de son fichier source.
- En général, il n'est pas nécessaire d'utiliser directement ces classes, car les fonctions `use`, `require` et `load` choisiront entre elles et les sources plus récentes.
- Une classe de chargement est générée pour chaque fichier référencé lors de la compilation d'un espace de noms, si son fichier `.class` de chargement est plus ancien que sa source.
- Un outil de génération de classes autonome permet de créer des classes nommées directement utilisables comme classes Java, avec les fonctionnalités suivantes :
- Nommer la classe générée
- Sélectionner la superclasse
- Spécifier les interfaces implémentées
- Spécifier les signatures des constructeurs
- Spécifier l'état
- Déclarer des méthodes supplémentaires
- Générer des méthodes de fabrique statiques
- Générer la méthode `main`
- Contrôler le mappage vers un espace de noms d'implémentation
- Exposer les membres protégés hérités
- Générer plusieurs classes nommées à partir d'un seul fichier, avec des implémentations dans un ou plusieurs espaces de noms
- Une directive optionnelle `:gen-class` peut être utilisée dans la déclaration `ns` pour générer une classe nommée correspondant à un espace de noms. Par défaut, lorsqu'elle est fournie, `:gen-class ...` prend les valeurs suivantes : `:name` (correspondant au nom de `ns`), `:main` défini sur `true`, `:impl-ns` identique à `ns` et `:init-impl-ns` défini sur `true`. Toutes les options de `gen-class` sont prises en charge.
- Les directives `gen-class` et `:gen-class` sont ignorées lors de la non-compilation.
- Un module `gen-interface` autonome est fourni pour générer des classes d'interface nommées, utilisables directement comme interfaces Java. Ce module permet notamment de :
- Nommer l'interface générée
- Spécifier les superinterfaces
- Déclarer les signatures des méthodes de l'interface
Compilation
Pour compiler une bibliothèque, utilisez la fonction `compile` et spécifiez le nom de l'espace de noms sous forme de symbole. Pour l'espace de noms `my.domain.lib`, défini dans `my/domain/lib.clj` et présent dans le classpath, le processus suivant se déroulera :
- Un fichier de classe de chargement sera créé dans `my/domain/lib__init.class`, sous `*compile-path*`, qui doit figurer dans le classpath.
- Un ensemble de fichiers de classe sera généré, un par fonction (`fn`) de l'espace de noms, avec des noms tels que `my/domain/lib$fnname__1234.class`.
- Pour chaque classe générée :
- Un fichier de classe stub sera créé avec le nom spécifié.
Options du compilateur
Le compilateur Clojure peut être contrôlé à l'aide de plusieurs options de compilation. Lors de l'exécution, celles-ci sont stockées dans la variable dynamique `clojure.core/*compiler-options*`, qui est une table de hachage contenant les clés de mots-clés optionnelles suivantes :
- :disable-locals-clearing (booléen)
- :elide-meta (vecteur de mots-clefs)
- :direct-linking (booléen)
Ces options du compilateur peuvent être modifiées dynamiquement lors de l'appel à la fonction de compilation afin de modifier son comportement.
Il est également possible de définir les options du compilateur via les propriétés système Java au démarrage.
- -Dclojure.compiler.disable-locals-clearing=true
- "-Dclojure.compiler.elide-meta=[:doc :file :line :added]"
- -Dclojure.compiler.direct-linking=true
Voir ci-dessous pour plus d'informations sur chacune de ces options.
Nettoyage local
Par défaut, le compilateur Clojure génère du code qui supprime immédiatement les références du ramasse-miettes aux liaisons locales. Cependant, lors de l'utilisation d'un débogueur, ces variables locales apparaissent comme nulles, ce qui complique le débogage. L'option `disable-locals-clearing=true` empêche la suppression des variables locales. Il est déconseillé de désactiver cette suppression pour la compilation en vue de la production.
Supprimer les métadonnées
Les métadonnées (docstrings, informations sur les fichiers et les lignes,...) seront compilées en chaînes de caractères dans le bassin de constantes des classes compilées. Pour réduire la taille des classes et accélérer leur chargement, il est possible de supprimer les métadonnées. Cette option prend un vecteur de mots-clefs à supprimer, comme `:doc`, `:file`, `:line` et `:added`. Attention : la suppression des métadonnées peut rendre certaines fonctionnalités inopérantes (par exemple, la fonction `doc` ne peut pas renvoyer les docstrings si elles ont été supprimées).
Liaison directe
Normalement, l'appel d'une fonction entraîne le déréférencement d'une variable pour trouver l'instance de la fonction qui l'implémente, puis l'appel de cette fonction. Cette indirection via la variable est l'un des mécanismes par lesquels Clojure fournit un environnement d'exécution dynamique. Cependant, on constate depuis longtemps que la majorité des appels de fonction en production ne sont jamais redéfinis de cette manière, ce qui engendre des redirections inutiles.
La liaison directe permet de remplacer cette indirection par un appel statique direct à la fonction. L'appel de variable s'en trouve accéléré. De plus, le compilateur peut supprimer les variables inutilisées lors de l'initialisation des classes, et la liaison directe rendra encore plus de variables inutilisées. Il en résulte généralement des classes plus petites et des temps de démarrage plus rapides.
L'une des conséquences de la liaison directe est que les redéfinitions de variables ne seront pas visibles par le code compilé avec cette méthode (car la liaison directe évite le déréférencement de la variable). Les variables marquées comme `^:dynamic` ne seront jamais liées directement. Si vous souhaitez indiquer qu'une variable supporte la redéfinition (mais pas le caractère dynamique), utilisez `^:redef` pour éviter la liaison directe.
Depuis Clojure 1.8, la bibliothèque principale de Clojure est compilée avec liaison directe.
Exécution
Les classes générées par Clojure sont hautement dynamiques. En particulier, il est important de noter qu'aucun corps de méthode ni autre détail d'implémentation n'est spécifié dans `gen-class` ; seule une signature est spécifiée, et la classe générée est un stub. Cette classe stub délègue toute l'implémentation aux fonctions définies dans l'espace de noms `implementing`. À l'exécution, un appel à une méthode `foo` de la classe générée trouvera la valeur actuelle de la variable `implementing.namespace/prefixfoo` et l'appellera. Si la variable n'est pas définie ou est nulle, la méthode de la superclasse sera appelée ; s'il s'agit d'une méthode d'interface, une exception `UnsupportedOperationException` sera levée.
Exemples de gen-class
Dans le cas le plus simple, une `:gen-class` vide est fournie, et la classe compilée ne contient que la méthode `main`, implémentée en définissant `-main` dans l'espace de noms. Le fichier doit être enregistré dans `src/clojure/examples/hello.clj` :
- (ns clojure.examples.hello
- (:gen-class))
-
- (defn -main
- [greetee]
- (println (str "Bonjour " greetee "!")))
Pour compiler, assurez-vous que le répertoire de sortie cible «classes» existe :
| mkdir classes |
Créez ensuite un fichier deps.edn décrivant votre classpath :
- {:paths ["src" "classes"]}
Compilez ensuite pour générer les classes comme suit :
|
$ clj Clojure 1.10.1 user=> (compile 'clojure.examples.hello) clojure.examples.hello |
Et peut être exécuté comme une application Java ordinaire, comme ceci (veillez à inclure le répertoire des classes de sortie) :
|
java -cp `clj -Spath` clojure.examples.hello Fred Bonjour Fred! |
Voici un exemple utilisant à la fois une classe `:gen-class` plus complexe et des appels autonomes à `gen-class` et `gen-interface`. Ici, nous créons des classes dont nous allons créer des instances. La classe `clojure.examples.instance` implémentera `java.util.Iterator`, une interface particulièrement complexe car elle exige que l'implémentation soit à état. Cette classe prendra une chaîne de caractères dans son constructeur et implémentera l'interface `Iterator` pour la transmission des caractères de cette chaîne. La clause `:init` spécifie la fonction constructeur. La clause `:constructors` est une correspondance entre la signature du constructeur et celle de la superclasse. Dans ce cas, la superclasse est par défaut `Object`, dont le constructeur ne prend aucun argument. Cet objet aura un état, appelé `state`, et une méthode `main` pour permettre ses tests.
Les fonctions `:init` (ici, `-init`) sont particulières car elles renvoient toujours un vecteur dont le premier élément est un vecteur d'arguments pour le constructeur de la superclasse. Comme notre superclasse ne prend aucun argument, ce vecteur est vide. Le deuxième élément du vecteur représente l'état de l'instance. Puisque nous devrons modifier cet état (qui est toujours final), nous utiliserons une référence à une map contenant la chaîne de caractères et l'index courant.
Les méthodes `hasNext` et `next` implémentent des méthodes de l'interface `Iterator`. Bien que ces méthodes ne prennent aucun argument, leurs fonctions d'implémentation prennent toujours un premier argument supplémentaire, correspondant à l'objet sur lequel la méthode est appelée (appelé ici par convention `this`). Notez que l'état est accessible via un champ Java classique.
L'appel `gen-interface` crée une interface nommée `clojure.examples.IBar`, avec une seule méthode `bar`.
L'appel `gen-class` génère une autre classe nommée, `clojure.examples.impl`, dont l'espace de noms d'implémentation sera par défaut l'espace de noms courant. Cette classe implémente `clojure.examples.IBar`. L'option `:prefix` permet d'associer les fonctions d'implémentation des méthodes à des fonctions commençant par `impl-` au lieu de `-`. L'option `:methods` définit une nouvelle méthode `foo` absente des superclasses et interfaces.
Notez dans la fonction `main` comment créer des instances des classes et appeler leurs méthodes grâce à l'interopérabilité Java standard. Leur utilisation est tout aussi classique en Java.
- (ns clojure.examples.instance
- (:gen-class
- :implements [java.util.Iterator]
- :init init
- :constructors {[String] []}
- :state state))
-
- (defn -init [s]
- [[] (ref {:s s :index 0})])
-
- (defn -hasNext [this]
- (let [{:keys [s index]} @(.state this)]
- (< index (count s))))
-
- (defn -next [this]
- (let [{:keys [s index]} @(.state this)
- ch (.charAt s index)]
- (dosync (alter (.state this) assoc :index (inc index)))
- ch))
-
- (gen-interface
- :name clojure.examples.IBar
- :methods [[bar [] String]])
-
- (gen-class
- :name clojure.examples.impl
- :implements [clojure.examples.IBar]
- :prefix "impl-"
- :methods [[foo [] String]])
-
- (defn impl-foo [this]
- (str (class this)))
-
- (defn impl-bar [this]
- (str "I " (if (instance? clojure.examples.IBar this)
- "am"
- "am not")
- " an IBar"))
-
- (defn -main [s]
- (let [x (new clojure.examples.instance s)
- y (new clojure.examples.impl)]
- (while (.hasNext x)
- (println (.next x)))
- (println (.foo y))
- (println (.bar y))))
Compiler comme ci-dessus :
|
$ clj Clojure 1.10.1 user=> (compile 'clojure.examples.instance) clojure.examples.instance |
Et s'exécuter comme une application Java ordinaire :
|
java -cp `clj -Spath` clojure.examples.instance asdf a s d f class clojure.examples.impl I am an IBar |