Find, cache and bind a prebuilt native library — the boring half of every Clojure FFM wrapper, factored out.
A library like com.blockether/imaging ships a Rust cdylib. The cdylib itself
is published as per-platform artifacts (imaging-native-darwin-arm64, …), each
carrying prebuilds/<platform>/<file> on the classpath. At runtime the wrapper
must decide WHICH file to dlopen, and it must work in three very different
situations: a dev pointing at a freshly cargo builded library, a fat uberjar
or GraalVM native image with the library bundled, and a plain deps.edn user
who only declared the pure-Clojure jar.
That resolution order is identical in every such library, so it lives here:
<LIB>_NATIVE_PATH env or <group>.<lib>.native.path
system property. Used verbatim, no existence check, no download.prebuilds/<platform>/<file>. This is the
uberjar / native-image path; a resource inside a jar is copied to a
temp file (a dlopen needs a real path), a directory resource is used
in place.<lib>-native-<platform> artifact resolved
through clojure.tools.deps (so Maven repos, mirrors and
~/.m2/settings.xml are honoured), extracted into
~/.cache/clj-<lib>/<version>/<platform>/ and cached there. Disable with
<LIB>_DISABLE_DOWNLOAD=1.tools.deps is reached through requiring-resolve, so it is only touched on
step 3 and never becomes a hard dependency (it does not fit in a native image).
This library itself has ZERO dependencies.
Everything is driven by a spec map built by spec; every derived name has a
default and can be overridden:
(def ^:private lib (nr/spec {:lib "imaging"})) ;; => env IMAGING_NATIVE_PATH / IMAGING_CACHE_DIR / IMAGING_DISABLE_DOWNLOAD ;; prop com.blockether.imaging.native.path / .cache-dir / .disable-download ;; file libimaging_c.dylib | libimaging_c.so | imaging_c.dll ;; artifact com.blockether/imaging-native-<platform> ;; version from the classpath resource imaging/VERSION ;; cache ~/.cache/clj-imaging
And the binding half:
(defonce ^:private fns (delay (nr/bind! lib {:version [:ptr "imaging_version"] :decode [:ptr "imaging_decode" :ptr :i64] :free [nil "imaging_free_string" :ptr]})))
(nr/invoke @fns :decode seg (count bs))
Run the JVM with --enable-native-access=ALL-UNNAMED so the foreign linker may
load the library without a restricted-method warning. Requires JDK 22+ (the
final FFM API); JDK 25 is what we test on.
Find, cache and bind a prebuilt native library — the boring half of every
Clojure FFM wrapper, factored out.
A library like `com.blockether/imaging` ships a Rust cdylib. The cdylib itself
is published as per-platform artifacts (`imaging-native-darwin-arm64`, …), each
carrying `prebuilds/<platform>/<file>` on the classpath. At runtime the wrapper
must decide WHICH file to `dlopen`, and it must work in three very different
situations: a dev pointing at a freshly `cargo build`ed library, a fat uberjar
or GraalVM native image with the library bundled, and a plain `deps.edn` user
who only declared the pure-Clojure jar.
That resolution order is identical in every such library, so it lives here:
1. An explicit path — `<LIB>_NATIVE_PATH` env or `<group>.<lib>.native.path`
system property. Used verbatim, no existence check, no download.
2. A bundled classpath resource `prebuilds/<platform>/<file>`. This is the
uberjar / native-image path; a resource inside a jar is copied to a
temp file (a `dlopen` needs a real path), a directory resource is used
in place.
3. A runtime download: the `<lib>-native-<platform>` artifact resolved
through `clojure.tools.deps` (so Maven repos, mirrors and
`~/.m2/settings.xml` are honoured), extracted into
`~/.cache/clj-<lib>/<version>/<platform>/` and cached there. Disable with
`<LIB>_DISABLE_DOWNLOAD=1`.
tools.deps is reached through `requiring-resolve`, so it is only touched on
step 3 and never becomes a hard dependency (it does not fit in a native image).
This library itself has ZERO dependencies.
Everything is driven by a spec map built by `spec`; every derived name has a
default and can be overridden:
(def ^:private lib (nr/spec {:lib "imaging"}))
;; => env IMAGING_NATIVE_PATH / IMAGING_CACHE_DIR / IMAGING_DISABLE_DOWNLOAD
;; prop com.blockether.imaging.native.path / .cache-dir / .disable-download
;; file libimaging_c.dylib | libimaging_c.so | imaging_c.dll
;; artifact com.blockether/imaging-native-<platform>
;; version from the classpath resource imaging/VERSION
;; cache ~/.cache/clj-imaging
And the binding half:
(defonce ^:private fns
(delay (nr/bind! lib {:version [:ptr "imaging_version"]
:decode [:ptr "imaging_decode" :ptr :i64]
:free [nil "imaging_free_string" :ptr]})))
(nr/invoke @fns :decode seg (count bs))
Run the JVM with `--enable-native-access=ALL-UNNAMED` so the foreign linker may
load the library without a restricted-method warning. Requires JDK 22+ (the
final FFM API); JDK 25 is what we test on.(artifact spec)(artifact {:keys [group artifact-prefix]} platform-id)The per-platform native artifact as a qualified symbol, e.g.
com.blockether/imaging-native-darwin-arm64.
The per-platform native artifact as a qualified symbol, e.g. `com.blockether/imaging-native-darwin-arm64`.
(bind! spec sigs)(bind! spec sigs arena)Resolve the library and bind every signature in sigs, returning
{key MethodHandle}.
A signature is [ret sym-name & arg-layouts] — ret nil for void:
(bind! spec {:version [:ptr "imaging_version"] :decode [:ptr "imaging_decode" :ptr :i64] :free [nil "imaging_free_string" :ptr]})
Do this once, behind a delay/defonce, so the library is opened lazily.
Resolve the library and bind every signature in `sigs`, returning
`{key MethodHandle}`.
A signature is `[ret sym-name & arg-layouts]` — `ret` nil for void:
(bind! spec {:version [:ptr "imaging_version"]
:decode [:ptr "imaging_decode" :ptr :i64]
:free [nil "imaging_free_string" :ptr]})
Do this once, behind a `delay`/`defonce`, so the library is opened lazily.(cache-root {:keys [cache-dir cache-name] :as spec})Where downloaded libraries are cached: <LIB>_CACHE_DIR env,
<group>.<lib>.cache-dir property, the spec's :cache-dir, else
~/.cache/clj-<lib>.
Where downloaded libraries are cached: `<LIB>_CACHE_DIR` env, `<group>.<lib>.cache-dir` property, the spec's `:cache-dir`, else `~/.cache/clj-<lib>`.
(downcall lookup sym-name desc)One MethodHandle for symbol sym-name in lookup with descriptor desc.
One `MethodHandle` for symbol `sym-name` in `lookup` with descriptor `desc`.
(extract-native! jar-path res dest)Copy the entry res out of the jar at jar-path to dest, creating parents.
Returns dest.
Copy the entry `res` out of the jar at `jar-path` to `dest`, creating parents. Returns `dest`.
(fd ret & args)A FunctionDescriptor: ret (a layout keyword or nil for void) then the
argument layouts.
A `FunctionDescriptor`: `ret` (a layout keyword or nil for void) then the argument layouts.
(invoke handles k & args)Call the handle at k in a bind! map.
Call the handle at `k` in a `bind!` map.
(layout x)A MemoryLayout from a keyword (:ptr :i8 :i16 :i32 :i64 :f32 :f64 :bool :char plus :addr :int :long :size :float :double :u32 :u64 :byte :short
aliases) or a MemoryLayout passed through.
A `MemoryLayout` from a keyword (`:ptr :i8 :i16 :i32 :i64 :f32 :f64 :bool :char` plus `:addr :int :long :size :float :double :u32 :u64 :byte :short` aliases) or a `MemoryLayout` passed through.
(library-file-name os base)The platform's file name for the cdylib base base ("imaging_c" ->
libimaging_c.dylib / libimaging_c.so / imaging_c.dll).
The platform's file name for the cdylib base `base` ("imaging_c" ->
libimaging_c.dylib / libimaging_c.so / imaging_c.dll).(library-path spec)Just the resolved Path — see resolve-library.
Just the resolved `Path` — see `resolve-library`.
(lookup spec)(lookup spec arena)A SymbolLookup over the resolved library.
The arena defaults to Arena/ofAuto — GC-managed and process-lifetime,
deliberately NOT Arena/ofShared: a shared arena is incompatible with Truffle
runtime compilation, so a native image that also embeds GraalPy fails to build
with "Arena.ofShared is not supported with runtime compilations". ofAuto
needs no flag and keeps the lookup and its downcall handles alive as long as
they are reachable.
A `SymbolLookup` over the resolved library. The arena defaults to `Arena/ofAuto` — GC-managed and process-lifetime, deliberately NOT `Arena/ofShared`: a shared arena is incompatible with Truffle runtime compilation, so a native image that also embeds GraalPy fails to build with "Arena.ofShared is not supported with runtime compilations". `ofAuto` needs no flag and keeps the lookup and its downcall handles alive as long as they are reachable.
(platform)This JVM's platform as {:os :arch :id}, e.g.
{:os "darwin" :arch "arm64" :id "darwin-arm64"}.
:os is one of darwin/linux/windows, :arch one of arm64/x64. Throws
ex-info on anything else — a native library that has no build for the host
should say so loudly rather than guess.
This JVM's platform as `{:os :arch :id}`, e.g.
`{:os "darwin" :arch "arm64" :id "darwin-arm64"}`.
`:os` is one of darwin/linux/windows, `:arch` one of arm64/x64. Throws
`ex-info` on anything else — a native library that has no build for the host
should say so loudly rather than guess.(platform-id spec)The platform id this spec resolves for — its :platform override, else the
host's.
The platform id this spec resolves for — its `:platform` override, else the host's.
(resolve-library spec-map)Resolve the native library for spec-map, returning
{:path Path :source :configured|:bundled|:downloaded :platform id :resource res}.
Throws ex-info (with :platform, :resource, :artifact) when no source
has it.
Resolve the native library for `spec-map`, returning
`{:path Path :source :configured|:bundled|:downloaded :platform id :resource res}`.
Throws `ex-info` (with `:platform`, `:resource`, `:artifact`) when no source
has it.(resource-name spec)The classpath resource a bundled library lives at:
prebuilds/<platform>/<file>.
The classpath resource a bundled library lives at: `prebuilds/<platform>/<file>`.
(spec {:keys [lib group lib-base lib-files artifact-prefix env-prefix
prop-prefix version version-resource resource-prefix cache-name
cache-dir download? resolve-jar]
:as m})Normalise a resolver spec. :lib (a short id like "imaging") is the only
required key; everything else is derived from it and overridable:
:lib short library id, e.g. "imaging" (required) :group Maven group id "com.blockether" :lib-base cdylib base name "<lib>_c" :lib-files explicit {os file-name} map, wins over :lib-base :artifact-prefix native artifact id prefix "<lib>-native-" :env-prefix env var prefix SCREAMING <lib> :prop-prefix system property prefix "<group>.<lib>" :version version string, skips the resource lookup :version-resource classpath resource holding it "<lib>/VERSION" :resource-prefix classpath dir holding the prebuilds "prebuilds" :cache-name cache directory name under ~/.cache "clj-<lib>" :cache-dir explicit cache root, wins over :cache-name :platform platform id override (default: this host's) :download? allow the tools.deps fallback true :resolve-jar (fn [version platform-id] -> Path) for the native jar
A NAMESPACED version resource (imaging/VERSION) is the default on purpose: a
root VERSION collides with every other library that ships one, and whichever
lands first on the classpath wins — which makes a library resolve a FOREIGN
version and 404 a native artifact that never existed.
Normalise a resolver spec. `:lib` (a short id like "imaging") is the only
required key; everything else is derived from it and overridable:
:lib short library id, e.g. "imaging" (required)
:group Maven group id "com.blockether"
:lib-base cdylib base name "<lib>_c"
:lib-files explicit {os file-name} map, wins over :lib-base
:artifact-prefix native artifact id prefix "<lib>-native-"
:env-prefix env var prefix SCREAMING <lib>
:prop-prefix system property prefix "<group>.<lib>"
:version version string, skips the resource lookup
:version-resource classpath resource holding it "<lib>/VERSION"
:resource-prefix classpath dir holding the prebuilds "prebuilds"
:cache-name cache directory name under ~/.cache "clj-<lib>"
:cache-dir explicit cache root, wins over :cache-name
:platform platform id override (default: this host's)
:download? allow the tools.deps fallback true
:resolve-jar (fn [version platform-id] -> Path) for the native jar
A NAMESPACED version resource (`imaging/VERSION`) is the default on purpose: a
root `VERSION` collides with every other library that ships one, and whichever
lands first on the classpath wins — which makes a library resolve a FOREIGN
version and 404 a native artifact that never existed.(version {:keys [version version-resource] :as spec})The version used to resolve the native artifact: the spec's :version, else
the trimmed :version-resource from the classpath.
The version used to resolve the native artifact: the spec's `:version`, else the trimmed `:version-resource` from the classpath.
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |