See also ticket CLJS-27.
Tools like lein-cljsbuild provide some help using "crossover" files. Crossover files are usually written in Clojure and contain only code that works on both platforms. On compilation the crossover files are run through a preprocessor that removes the special comment ;*CLJSBUILD-REMOVE*; from the source file to allow platform specific references to macros. This is explained in detail over here:
cljx is an implementation of feature expressions that:
.cljxfiles containing feature expression-annotated code into
.cljsfiles for consumption by other tools/compilers/etc
- optionally applies the same transformation interactively via installation of a REPL extension
It has been used successfully by a number of projects (see the cljx README for a partial list). cljx's limitations include:
- It does not address portability of macros at all; it is strictly a source-to-source transformation. Macros continue to be written in Clojure, and must be rewritten or implemented conditionally on the contents of
- It does not provide any runtime customization of the "features" you can use; these are set either within build configuration (via cljx' Leiningen integration), or via the configuration of the cljx REPL extension. The latter technically is available for modification, but is not in practical use.
- The set of provided "features" is limited to one for Clojure (
#+clj) and one for ClojureScript (
#+cljs). Further discrimination based on target runtime (e.g. rhino vs. node vs. v8 vs. CLR) would be trivial, but has not been implemented to date.
lein-cljsbuild provides a (deprecated, to be removed) feature called "crossovers" that provides a very limited preprocessing of certain files during the cljsbuild build process; a special comment string is removed, allowing one to work around the
-macros declarations required in ClojureScript
ns forms. Crossover files must otherwise be fully portable. Language/runtime-specific code must be maintained in separate files. However, (my) experience shows that this can quickly lead to the situation where one has to think a lot about in which file to put a specific function, in order to go though the whole preprocessing machinery. Functions are split into namespaces because of conditional compilation, and not because they belong to the same part or module of the program.
As mentioned above the granularity where this solution solves the problem is probably not at the right level. Having only one function with a platform specific form puts one into the business of splitting it into two files maintaining two different versions of the function. A branching granularity on a form level would probably be much better.
A solution to this problem that is used by Common Lisp implementations are feature expressions. Each platform has a variable called
*features* that contains keywords that indicate the supported features of the platform the code is running under. The branching on a platform or a platform specific feature is done via the reader macros
#- followed by a feature condition. The feature condition is either a symbol or a form that combines symbols with the
not operators. The feature condition is evaluated by looking up the symbols in the
*features* variable. If the feature condition evaluates to true the next form will be passed through the reader and evaluated, otherwise it will be discarded.
The patches attached to the CLJS-27 ticket contain a proof of concept implementation of these feature expressions for Clojure and ClojureScript. With this extension one can branch on a form level an and write code like illustrated by the following example: