Section courante

A propos

Section administrative du site

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 :

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 :

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 :

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 :

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.

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` :

  1. (ns clojure.examples.hello
  2.     (:gen-class))
  3.  
  4. (defn -main
  5.   [greetee]
  6.   (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 :

  1. {: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.

  1. (ns clojure.examples.instance
  2.     (:gen-class
  3.      :implements [java.util.Iterator]
  4.      :init init
  5.      :constructors {[String] []}
  6.      :state state))
  7.  
  8. (defn -init [s]
  9.   [[] (ref {:s s :index 0})])
  10.  
  11. (defn -hasNext [this]
  12.   (let [{:keys [s index]} @(.state this)]
  13.     (< index (count s))))
  14.  
  15. (defn -next [this]
  16.   (let [{:keys [s index]} @(.state this)
  17.         ch (.charAt s index)]
  18.     (dosync (alter (.state this) assoc :index (inc index)))
  19.     ch))
  20.  
  21. (gen-interface
  22.  :name clojure.examples.IBar
  23.  :methods [[bar [] String]])
  24.  
  25. (gen-class
  26.  :name clojure.examples.impl
  27.  :implements [clojure.examples.IBar]
  28.  :prefix "impl-"
  29.  :methods [[foo [] String]])
  30.  
  31. (defn impl-foo [this]
  32.   (str (class this)))
  33.  
  34. (defn impl-bar [this]
  35.   (str "I " (if (instance? clojure.examples.IBar this)
  36.               "am"
  37.               "am not")
  38.        " an IBar"))
  39.  
  40. (defn -main [s]
  41.   (let [x (new clojure.examples.instance s)
  42.         y (new clojure.examples.impl)]
  43.     (while (.hasNext x)
  44.       (println (.next x)))
  45.     (println (.foo y))
  46.     (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


Dernière mise à jour : Lundi, le 2 février 2026