Clojure + Crema: a native binary that runs full JVM Clojure with fast startup.
Cream uses GraalVM's Crema (RuntimeClassLoading) to enable runtime eval,
require, and library loading in a native binary. It can also run Java source
files directly, as a fast alternative to
JBang.
Warning: Cream is very alpha. It depends on GraalVM Crema (EA) and a custom Clojure fork. Do not use in production. Issues and ideas are welcome though: https://github.com/borkdude/cream/issues
Install
Download from the latest dev release:
# macOS (Apple Silicon) curl -sL https://github.com/borkdude/cream/releases/download/dev/cream-macos-aarch64.tar.gz | tar xz # Linux (x86_64) curl -sL https://github.com/borkdude/cream/releases/download/dev/cream-linux-amd64.tar.gz | tar xz # Windows (PowerShell) # Invoke-WebRequest -Uri https://github.com/borkdude/cream/releases/download/dev/cream-windows-amd64.zip -OutFile cream.zip # Expand-Archive cream.zip -DestinationPath . sudo mv cream /usr/local/bin/ # macOS/Linux
Or build from source (see Building from source).
Quick start
$ ./cream -M -e '(+ 1 2 3)'
6Runtime type creation
Unlike babashka, cream supports definterface, deftype, gen-class, and
other constructs that generate JVM bytecode at runtime:
$ ./cream -M -e '(do (definterface IGreet (greet [name])) (deftype Greeter [] IGreet (greet [_ name] (str "Hello, " name))) (.greet (Greeter.) "world"))' "Hello, world"
Loading libraries at runtime
Use -Scp to add JARs to the classpath:
./cream -Scp "$(clojure -Spath -Sdeps '{:deps {org.clojure/data.json {:mvn/version "RELEASE"}}}')" \ -M -e '(do (require (quote [clojure.data.json :as json])) (json/write-str {:a 1}))' ;; => "{\"a\":1}"
Running Java files
Cream can run .java source files directly, compiling and caching them
automatically. This makes it a fast alternative to JBang.
public class Hello { public static void main(String[] args) { System.out.println("Hello from Java!"); if (args.length > 0) { System.out.println("Args: " + String.join(", ", args)); } } }
$ ./cream /tmp/Hello.java Hello from Java! $ ./cream /tmp/Hello.java world Hello from Java! Args: world
Dependencies
Use //DEPS comments (same syntax as JBang) to declare Maven dependencies:
//DEPS commons-codec:commons-codec:1.17.1 import org.apache.commons.codec.binary.Hex; import org.apache.commons.codec.digest.DigestUtils; public class HelloCodec { public static void main(String[] args) { String input = args.length > 0 ? args[0] : "hello world"; System.out.println("Input: " + input); System.out.println("Hex: " + Hex.encodeHexString(input.getBytes())); System.out.println("SHA-256: " + DigestUtils.sha256Hex(input)); } }
$ time ./cream /tmp/HelloCodec.java
Input: hello world
Hex: 68656c6c6f20776f726c64
SHA-256: b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9
./cream /tmp/HelloCodec.java 0.02s user 0.01s system 93% cpu 0.031 totalDependencies are resolved from Maven Central using
deps.clj, with no external tooling
required. Compiled classes and resolved classpaths are cached under
$XDG_CACHE_HOME/cream/ (defaulting to ~/.cache/cream/) so subsequent runs
skip compilation and dependency resolution.
Requirements
Some Java interop may require JAVA_HOME pointing to a JDK installation, as
Crema loads certain classes at runtime from the JDK's lib/modules (JRT
filesystem). Pure Clojure code works without JAVA_HOME.
Known limitations
- May need
JAVA_HOMEfor Java interop (some JDK classes are loaded at runtime; pure Clojure works without it) - Requires a lightly patched Clojure fork (minor workarounds for Crema
quirks in
RT.java,Var.java, andCompiler.java, details) - Java enum support fixed in GraalVM ea17 (oracle/graal#13034)
Class.forNamenot dispatchable (oracle/graal#13031: GraalVM inlinesClass.forNamesubstitutions at call sites, so Crema's interpreter can't dispatch to it; the Clojure fork redirects toRT.classForNameas a workaround, but Java.classfiles callingClass.forNamedirectly will still fail)- Large binary (~300MB, includes Crema interpreter and preserved packages)
- Crema is EA (GraalVM's RuntimeClassLoading is experimental and only available in EA builds)
See doc/technical.md for the full list of known issues and workarounds.
Cream vs Babashka
| Cream | Babashka | |
|---|---|---|
| Clojure | Full JVM Clojure (1.13 fork) | SCI interpreter (subset) |
| Library loading | Any library from JARs at runtime (except enum/Class.forName issues) | Any library (with built-in classes, SCI/deftype limitations) |
| Java interop | Full (runtime class loading) | Limited to compiled-in classes |
| Startup | ~20ms | ~20ms |
| Binary size | ~300MB | ~70MB |
| Standalone | Mostly (may need JAVA_HOME for Java interop) |
Yes |
| Loop 10M iterations* | ~720ms | ~270ms |
| Compile time (GitHub Actions, linux-amd64) | ~10min | ~3min |
| Maturity | Experimental | Production-ready |
* (time (loop [i 0] (when (< i 10000000) (recur (inc i)))))
Java interop is faster in cream since it calls methods directly rather than through SCI's reflection layer:
# 100K StringBuilder appends, cream is ~2x faster $ ./cream -M -e '(time (let [sb (StringBuilder.)] (dotimes [i 100000] (.append sb (str i))) (.length sb)))' "Elapsed time: 32 msecs" $ bb -e '(time (let [sb (StringBuilder.)] (dotimes [i 100000] (.append sb (str i))) (.length sb)))' "Elapsed time: 72 msecs"
Some perceived difference in loading/parsing time of pure clojure code and runtime performance. Some of these could be addressed by the addition of a JIT to Crema:
$ bb -cp "$(clojure -Spath -Sdeps '{:deps {dev.weavejester/medley {:mvn/version "1.9.0"}}}')" -e '(time (require (quote [medley.core :as mc]))) (time (dotimes [i 100000] (mc/greatest 5 2 1 3 4)))' "Elapsed time: 31.452928 msecs" "Elapsed time: 347.639424 msecs" $ ./cream -Scp "$(clojure -Spath -Sdeps '{:deps {dev.weavejester/medley {:mvn/version "1.9.0"}}}')" -M -e '(time (require (quote [medley.core :as mc]))) (time (dotimes [i 100000] (mc/greatest 5 2 1 3 4)))' Reflection warning, medley/core.cljc:519:25 - call to java.util.ArrayList ctor can't be resolved. "Elapsed time: 122.997416 msecs" "Elapsed time: 785.789744 msecs" $ ./cream -Scp "$(clojure -Spath -Sdeps '{:deps {camel-snake-kebab/camel-snake-kebab {:mvn/version "0.4.3"}}}')" -M -e '(time (require (quote [camel-snake-kebab.core :as csk]))) (time (dotimes [i 100000] (csk/->SCREAMING_SNAKE_CASE "I am constant")))' "Elapsed time: 124.247888 msecs" "Elapsed time: 11771.946679 msecs" $ bb -cp "$(clojure -Spath -Sdeps '{:deps {camel-snake-kebab/camel-snake-kebab {:mvn/version "0.4.3"}}}')" -e '(time (require (quote [camel-snake-kebab.core :as csk]))) (time (dotimes [i 100000] (csk/->SCREAMING_SNAKE_CASE "I am constant")))' "Elapsed time: 25.805933 msecs" "Elapsed time: 3285.816775 msecs"
When cream might make sense: you need full Clojure compatibility, arbitrary library loading, or Java interop beyond what babashka offers.
When babashka is better: scripting, tasks, CI glue, or anything where a standalone binary, fast startup, and a mature ecosystem matter.
Tested libraries
Libraries are tested against the cream binary using bb run-lib-tests.
| Library | CI | Status | Notes |
|---|---|---|---|
| data.csv | ✅ | Works | |
| data.json | ✅ | Works | |
| data.xml | Works | ||
| core.async | ✅ | Works | Some test ns skipped (ForkJoinPool segfault) |
| math.combinatorics | ✅ | Works | |
| tools.reader | ✅ | Works | |
| medley | ✅ | Works | |
| camel-snake-kebab | ✅ | Works | |
| hiccup | ✅ | Works | |
| deep-diff2 | ✅ | Works | |
| malli | Works | ||
| meander | Works | ||
| selmer | Works | ||
| specter | Works | ||
| tick | Works | ||
| clj-commons/fs | Works | ||
| prismatic/schema | ✅ | Works | |
| instaparse | ✅ | Works | |
| flatland/useful | Works | ||
| cheshire | ✅ | Works | Enum fix in ea17 |
| Jsoup | Works | HTML parsing | |
| http-kit | Partial | require works, server crashes on Selector.open() (needs java.nio.channels preserved) |
|
| clj-yaml | Blocked | Class.forName in SnakeYAML (GH-13031) |
Pure Clojure libraries generally work. Libraries using Java interop work when
the relevant packages are preserved. Libraries calling Class.forName from Java
bytecode are blocked by GH-13031.
Building from source
Requires a GraalVM EA build with RuntimeClassLoading support.
-
Install the custom Clojure fork:
git clone -b crema https://github.com/borkdude/clojure.git /tmp/clojure-fork cd /tmp/clojure-fork && mvn install -Dmaven.test.skip=true
-
Build the native binary:
GRAALVM_HOME=/path/to/graalvm bb build-native
Future work
- Fully standalone binary: investigate whether JRT metadata can be bundled
in the binary to eliminate the
JAVA_HOMErequirement for Java interop Class.forNamefix: blocked on oracle/graal#13031, would unblock clj-yaml- Reduce binary size: currently ~300MB due to preserved packages and Crema interpreter overhead
- nREPL support: enable interactive development with editor integration
Documentation
See doc/technical.md for implementation details, architecture decisions, and known issues.
License
Distributed under the EPL License. See LICENSE.
This project contains code from:
- Clojure, which is licensed under the same EPL License.