Coffi FFI library for calling native C code from Clojure via Panama FFM API (JDK 22+). Use when: wrapping native C libraries, calling native functions, working with off-heap memory and arenas, etc.
Coffi wraps the Panama Foreign Function & Memory API for calling native C code from Clojure.
Add to deps.edn:
;; use gh cli to check latest commit with `gh browse -c -n IGJoshua/coffi`
io.github.IGJoshua/coffi {:git/sha "ae3e38a449c88b998db98b0d4bffa9908dea1c79"}
JVM argument required:
--enable-native-access=ALL-UNNAMED
Or in deps.edn alias:
{:aliases {:dev {:jvm-opts ["--enable-native-access=ALL-UNNAMED"]}}}
Prep deps
clojure -Xdeps prep :aliases '[:dev :test]'
(require '[coffi.ffi :as ffi :refer [defcfn]]
'[coffi.mem :as mem])
;; Wrap a native function
(defcfn strlen
strlen [::mem/c-string] ::mem/long)
(strlen "hello") ;; => 5
;; Load a library
(ffi/load-system-library "z") ;; System library
(ffi/load-library "path/to/lib.so") ;; From path
(defcfn var-name
"docstring"
native_symbol_name [arg-types...] return-type)
;; With wrapper logic
(defcfn var-name
"native_symbol" [arg-types...] return-type
native-fn ;; Binds the raw native function
[clj-args...] ;; Clojure argument list
(body...)) ;; Wrapper body that calls native-fn
| Coffi Type | C Type | Notes |
|---|---|---|
::mem/byte | int8_t | |
::mem/short | int16_t | |
::mem/int | int32_t | |
::mem/long | int64_t | |
::mem/float | float | |
::mem/double | double | |
::mem/pointer | void* | |
::mem/c-string | char* | Null-terminated |
::mem/void | void | Return only |
[::mem/struct [...]] | struct | See below |
[::mem/array type n] | type[n] | Fixed size |
[::ffi/fn [args] ret] | function ptr | Callbacks |
For complete type reference: references/types.md
(require '[coffi.layout :as layout])
;; Always use layout/with-c-layout for FFI structs
(mem/defalias ::my-struct
(layout/with-c-layout
[::mem/struct
[[:name ::mem/c-string]
[:count ::mem/int]
[:value ::mem/double]]]))
;; Use in function
(defcfn process-data
process_data [::my-struct] ::mem/int)
(process-data {:name "test" :count 5 :value 3.14})
Always use confined-arena with with-open for temporary allocations:
(with-open [arena (mem/confined-arena)]
(let [ptr (mem/serialize data type arena)]
(native-fn ptr)))
;; Memory freed automatically
Arena types:
confined-arena - Thread-local, freed on close (most common)shared-arena - Multi-thread, freed on closeauto-arena - GC-managedglobal-arena - Never freedFor details: references/memory.md
(defcfn open-resource
"open_resource" [::mem/c-string ::mem/pointer] ::mem/int
native-open
[name]
(with-open [arena (mem/confined-arena)]
(let [out-ptr (mem/alloc-instance ::mem/pointer arena)
code (native-open name out-ptr)]
(if (zero? code)
(mem/deserialize-from out-ptr ::mem/pointer)
(throw (ex-info "Failed" {:code code}))))))
(defcfn bind-text
"bind_text" [::mem/pointer ::mem/c-string ::mem/int] ::mem/int
native-bind
[handle text]
(let [bytes (.getBytes text "UTF-8")]
(native-bind handle text (count bytes))))
;; Serialize array
(mem/serialize [1 2 3 4] [::mem/array ::mem/int 4] arena)
;; For raw Java arrays (better performance)
(mem/serialize (int-array [1 2 3 4]) [::mem/array ::mem/int 4 :raw? true] arena)
(defcfn set-callback
set_callback [[::ffi/fn [::mem/int] ::mem/int]] ::mem/void)
(set-callback (fn [x] (* x 2)))
For more examples: references/examples.md
Read these references depending on what you are doing. You should read at least one of the now, if not all of them