clove

FFI and the JIT

Both work by writing a little C, compiling it with the system's cc into a shared library, and loading that with dlopen. You need a C compiler at run time. Compiled modules are cached by a hash of their source, so only the first run pays for the compile. Neither works on Windows or in the browser.

Calling C

ffi/define takes C signatures as data and defines a clove function for each one.

(ffi/define [[:double cos [:double]]
             [:int abs [:int]]]
            {:headers ["math.h" "stdlib.h"] :libs "-lm"})

(cos 0.0)  ;=> 1.0
(abs -5)   ;=> 5

The types are :int, :double, :float, :string, :pointer and :void, plus structs by value once you've declared them with :structs. A pointer is an integer, and 0 is NULL. :libs goes to the compiler's command line, so "-lsqlite3" links SQLite.

ffi/defstruct writes the constructor and accessors for a C struct:

(ffi/defstruct timeval [[:int tv_sec] [:int tv_usec]] {:headers ["sys/time.h"]})
(ffi/define [[:int gettimeofday [:pointer :pointer]] [:void free [:pointer]]]
            {:headers ["sys/time.h" "stdlib.h"]})

(def tv (make-timeval))
(gettimeofday tv 0)
(timeval-tv_sec tv)   ;=> 1793642490
(free tv)

The repo has bigger examples. libc.clj binds a chunk of libc, examples/sqlite.clj talks to SQLite, and raylib.clj has enough of raylib to draw a bouncing ball (examples/raylib-bounce.clj).

Compiling hot functions

jit.clj compiles a numeric subset of clove to C: if, let, loop and recur, calls to itself, arithmetic and comparisons, Math/sqrt and friends. Define the function with jit/defn, then compile it, and the name now points at machine code.

(load-file "jit.clj")

(jit/defn fib [n]
  (if (< n 2) n (+ (fib (- n 1)) (fib (- n 2)))))

(time (fib 32))      ; "Elapsed time: 780 msecs"
(jit/compile! 'fib)
(time (fib 32))      ; "Elapsed time: 12 msecs"

Numbers are long long unless you hint ^double, on parameters or on the function name for its return type:

(jit/defn ^double dist [^double x ^double y]
  (Math/sqrt (+ (* x x) (* y y))))
(jit/compile! 'dist)
(dist 3.0 4.0)  ;=> 5.0

Anything outside the subset fails to compile, and the interpreted function stays in place:

(jit/defn shout [s] (str s "!"))
(jit/compile! 'shout)
;; jit: unsupported form (str s "!")