Cream offers a promising approach to creating native Clojure binaries with full JVM compatibility through GraalVM's Crema technology, though it currently exists in an experimental state with notable limitations compared to established solutions like babashka.
The emergence of Cream represents an intriguing development in the Clojure ecosystem, attempting to reconcile the tension between fast startup times and complete language compatibility. Created by borkdude, Cream leverages GraalVM's native-image compilation combined with Crema (RuntimeClassLoading) technology to produce a native binary that can run full JVM Clojure with impressive startup speeds of approximately 20 milliseconds. This positions Cream as a potential alternative to established solutions like babashka, while offering a fundamentally different approach to achieving native compilation for Clojure applications.
At its core, Cream's innovation lies in its ability to maintain runtime eval, require, and library loading capabilities within a native binary—a feat typically challenging with GraalVM'sAhead-of-Time (AOT) compilation. The project achieves this through Crema's RuntimeClassLoading feature, which allows for dynamic class loading that would normally be impossible in a compiled native binary. This technical approach enables Cream to support Clojure features that generate JVM bytecode at runtime, such as definterface, deftype, and gen-class, which are essential for many Clojure libraries and frameworks.
The installation process for Cream is straightforward, with pre-built binaries available for macOS (Apple Silicon), Linux (x86_64), and Windows. Users can quickly download and extract the binary, making it accessible for experimentation. However, the project currently carries a warning that it is in a very alpha state, depending on GraalVM Crema Early Access (EA) builds and a custom Clojure fork, which immediately signals to potential users that production use would be premature.
When examining Cream's performance characteristics, several interesting trade-offs emerge. While startup times are comparable to babashka at around 20ms, Cream's binary size is significantly larger at approximately 300MB compared to babashka's 70MB. This substantial difference stems from Cream's inclusion of the Crema interpreter and preserved packages necessary for runtime functionality. Performance benchmarks reveal a mixed picture: Cream demonstrates superior performance in Java interop operations—approximately 2x faster than babashka for operations like StringBuilder appends—due to its ability to call methods directly rather than through SCI's reflection layer. However, Cream shows slower performance for pure computation tasks, with loop benchmarks indicating that babashka completes 10 million iterations in about 270ms compared to Cream's 720ms.
The comparison between Cream and babashka reveals distinct philosophical approaches to solving the same problem of creating fast-starting Clojure executables. Babashka opts for a pragmatic solution, embedding a subset of Clojure through the SCI (Small Clojure Interpreter), which sacrifices some language features in exchange for a smaller binary size and production readiness. Cream, conversely, attempts to preserve full JVM Clojure compatibility at the expense of binary size and experimental status. This divergence suggests that neither solution is universally superior, but rather each serves different use cases and priorities.
Cream's library compatibility presents a nuanced picture. Pure Clojure libraries generally function without issues, as demonstrated by the project's testing of various popular libraries like data.csv, data.json, core.async, and others. However, libraries that depend on Java enums encounter failures due to a Crema limitation where enum.values() and EnumMap crash in the interpreter. This affects several commonly used libraries including http-kit, cheshire, and clj-yaml. The project's documentation acknowledges these limitations while suggesting that pure Clojure libraries typically work without problems, and Java-dependent libraries may function when the relevant packages are preserved during compilation.
The technical requirements for Cream highlight its experimental nature. Users may need to configure JAVA_HOME to point to a JDK installation for certain Java interop operations, as Crema loads certain JDK classes at runtime from the JRT filesystem. The project also requires a lightly patched Clojure fork with minor workarounds for Crema quirks in RT.java, Var.java, and Compiler.java. These dependencies underscore that Cream is not yet a drop-in replacement for standard Clojure runtime environments but rather a specialized solution for specific use cases.
Looking ahead, the Cream project outlines several areas for future improvement. The most significant is achieving a fully standalone binary that eliminates the JAVA_HOME requirement for Java interop. This would likely involve bundling JRT metadata within the binary itself. Another priority is addressing the enum support limitation, which is currently blocked on a Crema bug related to InterpreterResolvedObjectType.getDeclaredMethodsList() throwing an NPE. Reducing the binary size from its current 300MB would also enhance practical usability, though this may be challenging given the inherent overhead of including the Crema interpreter and preserved packages. Additionally, the project aims to add nREPL support to enable interactive development with editor integration, which would significantly improve the development experience.
The potential impact of Cream on the Clojure ecosystem could be substantial if it matures beyond its current experimental state. By offering a path to native compilation that preserves full language compatibility, Cream could enable new deployment scenarios for Clojure applications, particularly in environments where startup time is critical or where the full Clojure language and library ecosystem is required. The project's approach of leveraging GraalVM's experimental features also contributes to the broader exploration of native compilation for dynamic languages, potentially informing similar efforts in other language ecosystems.
However, Cream's current limitations suggest that it will not immediately displace established solutions like babashka. The binary size, experimental status, and compatibility issues with certain libraries make it impractical for many production use cases. Instead, Cream is likely to appeal to early adopters, researchers, and developers with specific needs that align with its strengths—particularly those requiring full JVM compatibility and advanced Java interop in a native binary.
The Cream project represents an ambitious exploration of the boundaries between native compilation and dynamic language runtime flexibility. Its success will depend not only on resolving its current technical limitations but also on the evolution of GraalVM's RuntimeClassLoading feature itself. As GraalVM continues to develop and stabilize its native compilation capabilities, Cream may find its footing as a valuable tool in the Clojure developer's arsenal, bridging the gap between the performance of native binaries and the expressive power of a complete Clojure runtime environment.
For developers interested in exploring Cream further, the project is available on GitHub, with installation instructions and detailed documentation about its architecture and known issues. The project welcomes contributions and feedback, particularly as it navigates the challenges of stabilizing an experimental technology while maintaining compatibility with the broader Clojure ecosystem.

Comments
Please log in or register to join the discussion