Cream
July 26, 2026 · View on GitHub
Implementation details and known issues. See the README for usage.
Custom Clojure fork
Branch crema of github.com/borkdude/clojure
(1.13.0-cream-SNAPSHOT).
git clone -b crema --depth 1 https://github.com/borkdude/clojure.git /tmp/clojure-fork
cd /tmp/clojure-fork && mvn install -Dmaven.test.skip=true
Fork changes
RT.java:
- Skip loading
clojure/coreat native-image runtime (already loaded at build time) usingorg.graalvm.nativeimage.imagecodeproperty check - Skip
doInit()at native-image runtime (user ns, refer, server were set up at build time) - Wrap
clojure.core.serverloading in!nativeImageRuntimeguard
Var.java:
- During build-time class init (
imagecode=buildtime),set!falls back tobindRoot()instead of throwing. Fixes "Can't change/establish root binding of: warn-on-reflection with set".
Architecture
GraalVM substitutions (src-java/Target_jdk_internal_misc_VM.java)
jdk.internal.misc.VM.initialize()— no-opjdk.internal.jrtfs.SystemImage.findHome()— returnsSystem.getProperty("java.home")to work aroundgetProtectionDomain().getCodeSource()issue for boot classesVM.getRuntimeArguments()substitution removed — 25e1 EA provides it internally (duplicate causes "conflicts with previously registered" error)
Class initialization
--initialize-at-build-time=clojure AOT-compiles Clojure core.
The Var.set() fork fix handles *warn-on-reflection*.
jdk.internal.jrtfs.SystemImage must be --initialize-at-run-time or the
analysis phase deadlocks.
Deterministic class initialization (ClojureFeature)
--initialize-at-build-time=clojure eagerly initializes all clojure.* classes
in parallel. This causes circular deadlocks: compiled Clojure classes reference
RT in <clinit>, while RT.<clinit> loads core which needs those classes.
ClojureFeature (a GraalVM Feature) forces RT.<clinit> to complete in
beforeAnalysis() on a single thread before parallel analysis starts. All core
namespaces, fn classes, and deftype classes get initialized sequentially. When
analysis later discovers them, they're already done — no deadlocks.
Earlier approaches modified PersistentTreeMap, MultiFn, Compiler, and
__init class generation to break circular deps. All reverted — the Feature
makes them unnecessary since Java's reentrant class init allows same-thread
access to partially-initialized classes.
Preserve packages
Preserved via -H:Preserve= in build_native.clj. A package missing here
shows up as Fatal error: Unable to call AOT method naming the method and the
package to add.
clojure.lang—creatorstatic field (functional interface support)java.lang,java.lang.invoke,java.lang.reflect—java.langcovers theStringConcatclasses thatinvokedynamicstring concatenation generatesjava.io—FilterInputStream, reading a subprocess's streamsjava.net,javax.net,javax.net.ssl— http-kit, including HTTPSjava.nio,java.nio.channels,java.nio.channels.spi,java.nio.charset,java.nio.filejava.util,java.util.concurrent,java.util.concurrent.atomic,java.util.concurrent.locks,java.util.function,java.util.jar,java.util.regex,java.util.stream,java.util.zip—java.util.streamfor the Closure compiler'sCollector.ofjava.security,java.text,java.time- module
java.logging, modulejava.sql—java.sqlforcljs.closure.--add-modules=java.sqldoes nothing on its own,Preserve=moduleis what includes the classes
The java.util.* entries beyond java.util itself, plus java.security,
java.text and java.time, were added while chasing tools.deps in-process.
That is blocked by oracle/graal#14075,
so they are not carrying their weight yet. bb/test_preserve.clj removes each
entry in turn to find which are load-bearing.
URL protocols
--enable-url-protocols=http,https,jar,unix — jar: is needed for
JarClassLoader.getResource() to construct jar:file:...!/... URLs.
JarClassLoader
Custom classloader extending DynamicClassLoader for native images.
URLClassLoader.findResource() doesn't work in Crema, so this reads JARs
via java.util.jar.JarFile directly.
- Indexes all JAR entries at construction for O(1) lookup
- Supports both JAR files and directories on classpath
- Falls back to parent classloader
Build-time namespace loading
Libraries that transitively depend on standard library namespaces (e.g.
data.json → pprint → clojure.walk) would fail because core fns like
use aren't reachable by native-image analysis.
All standard namespaces are required at build time in src/cream/main.clj,
so runtime require calls for them are no-ops.
clojure.reflect.java__init ordering
clojure.reflect.clj loads reflect/java via (load "reflect/java") from
source, so clojure.reflect.java__init is never class-initialized during
normal loading. When native-image discovers it later, it fails because the
TypeReference protocol isn't visible yet.
ClojureFeature.beforeAnalysis() forces the right order: RT → cream.main__init
(loads all standard namespaces including clojure.reflect) → clojure.reflect.java__init.
Reflection config
Generated by bb bb/gen_reflect_config.clj — ~470 classes based on babashka's
impl/classes.clj. All entries have allPublicMethods, allPublicConstructors,
allPublicFields enabled.
Known issues
-
Crema method handle bug (seen with stock Clojure 1.12.3):
ClassCastException: Integer cannot be cast to BooleaninMethodHandleUtils.intUnboxwhenReflector.canAccess()callsMethod.canAccess(Object)through a method handle. -
getRawAnnotationsnot implemented for runtime-loaded classes, which threwUnsupportedOperationExceptionand broke Clojure 1.13's@FunctionalInterfacedetection. Fixed upstream. The fork's guard was removed on ea.04. -
Enum support broken (oracle/graal#13034):
enum.values()andEnumMapcrash with NPE inInterpreterResolvedObjectType.getDeclaredMethodsList(). Blocks http-kit, cheshire, clj-yaml. -
Class.forNamenot dispatchable (oracle/graal#13031): fixed upstream by oracle/graal#13187. See below. -
SwitchBootstraps.typeSwitchnot dispatchable — same pattern asClass.forName: the bootstrap method for Java 21+ pattern matching switch expressions is substituted/inlined and not compiled as a standalone entry point. Affects Java libraries usingswitchwith pattern matching. -
Verifier NPE on a class/interface merge (oracle/graal#14075):
findLeastCommonAncestorwalks both hierarchies in lockstep, so an interface operand, which has no superclass, becomes null and is dereferenced on the next iteration. Only class files without aStackMapTable(major < 50) reach it, since newer ones carry frames the verifier reads instead of inferring. commons-logging 1.2 (major 46) hits it inLogFactory.getFactory, which mergesSecurityExceptionwithEnumeration, and that is on the Maven resolver's classpath, so-X:depsand-T:buildcrash. Repro atrepro/verifier-npe/. -
JAVA_HOMEis needed for classes Crema loads at runtime from the JDK'slib/modules. Clojure code and the tested libraries do not hit this. Such classes fail withClassNotFoundExceptionwhen no jimage is reachable.BootClassRegistrywarns on stdout the first time it consults the boot class loader without a jimage, throughLogUtils.warning, which usesSystem.out. Class loading is parent-first, so any runtime-generated class triggers it and the warning lands in the middle of program output.-mainforces that lookup at startup with stdout swallowed, which leaves theClassNotFoundExceptionas the only diagnostic.
Class.forName and GraalVM substitutions
Class.forName is internally substituted by GraalVM native-image. The
substitution is inlined at each call site — the original method is never
compiled as a standalone entry point. When Crema encounters
invokestatic java.lang.Class.forName(String) in runtime bytecode, there's
no compiled code to dispatch to.
What doesn't help:
- Adding
Class.forNamecalls in application code (inlined away) - Adding it to
reflect-config.json(reflection != method compilation) - Custom
@Substitute(conflicts with GraalVM's internal one)
What works: RT.classForName(String) — a non-substituted method that internally
calls the 3-arg Class.forName (inlined at compile time).
The Clojure fork redirected (Class/forName ...) interop to emit
invokestatic RT.classForName. That workaround was removed on ea.04, where
Class.forName dispatches from runtime-loaded bytecode.