Merge pull request #176 from jolt-lang/spike/foreign-callable
foreign-callable + standalone-binary bundling
This commit is contained in:
commit
d9d55fd533
17 changed files with 405 additions and 119 deletions
|
|
@ -56,11 +56,41 @@ roots, and de-sugars the argv into a run:
|
|||
- `-A:alias` → add the alias's paths/deps, then run the rest;
|
||||
- `repl` → a line REPL;
|
||||
- `path` → print the resolved roots;
|
||||
- `build -m NS [-o OUT] [--opt|--dev]` → AOT-compile the app into a standalone binary;
|
||||
- `<task>` → run a `deps.edn` `:tasks` entry.
|
||||
|
||||
The resolver lives in the overlay alongside the runtime, but the runtime's only
|
||||
dependency interface is the list of source roots it's handed.
|
||||
|
||||
## Native libraries
|
||||
|
||||
A library that binds C declares the shared objects it needs under `:jolt/native`,
|
||||
so `jolt.main` loads them before the namespace is required and its `foreign-fn`
|
||||
bindings resolve. Each entry is a map — `{:name "sqlite3" :darwin
|
||||
["libsqlite3.0.dylib" …] :linux ["libsqlite3.so.0" …]}` — with optional
|
||||
`:optional true` (absence is fine, a feature-gated dep) and `:process true` (use
|
||||
the running process's own symbols, e.g. libc sockets, no external file). A
|
||||
project inherits its dependencies' `:jolt/native`.
|
||||
|
||||
## Standalone binaries
|
||||
|
||||
`joltc build -m NS -o OUT` compiles the app and every library into one
|
||||
executable (the runtime + compiler are baked in). It loads the resolved
|
||||
`:jolt/native` libs at startup, so an FFI app — sockets, SQLite — runs with no
|
||||
jolt or Chez on the path.
|
||||
|
||||
`:jolt/build {:embed ["resources" …]}` bakes those directories' files into the
|
||||
binary; `io/resource` serves them from the image with no files on disk. Resources
|
||||
not embedded resolve at runtime against `JOLT_PWD` (or the cwd), so the
|
||||
ship-the-binary-with-its-`resources/`-dir model also works. Files read through
|
||||
`io/file` (e.g. a `config.edn` a config library loads) stay external by design —
|
||||
edit them without rebuilding.
|
||||
|
||||
A standalone build needs Chez's kernel dev files (`libkernel.a`, `scheme.h`) and
|
||||
a C compiler; `JOLT_CHEZ_CSV` overrides the auto-detected `csv<ver>/<machine>`
|
||||
dir. `--opt` turns on the inference/flatten/scalar-replace passes; the default
|
||||
`release` mode is const-fold only.
|
||||
|
||||
## Limitations
|
||||
|
||||
- Pure `clj`/`cljc` only — JVM interop, host classes, and unimplemented
|
||||
|
|
|
|||
|
|
@ -1,13 +0,0 @@
|
|||
(ns ffi-smoke
|
||||
(:require [jolt.ffi :as ffi]))
|
||||
(ffi/load-library "libglib-2.0.dylib")
|
||||
(prn :sizeof-pointer (ffi/sizeof :pointer))
|
||||
(prn :null (ffi/null))
|
||||
(prn :null? (ffi/null? (ffi/null)))
|
||||
(ffi/defcfn g-get-monotonic-time "g_get_monotonic_time" [] :int64)
|
||||
(prn :monotonic-time (g-get-monotonic-time))
|
||||
(let [p (ffi/alloc 16)]
|
||||
(ffi/write p :int64 0 42)
|
||||
(prn :wrote-and-read (ffi/read p :int64 0))
|
||||
(ffi/free p))
|
||||
(prn :done)
|
||||
|
|
@ -36,9 +36,12 @@ if ! JOLT_PWD="$app" bin/joltc build -m app.core -o "$out" >/dev/null 2>&1; then
|
|||
fi
|
||||
[ -x "$out" ] || { echo " FAIL: no executable produced"; exit 1; }
|
||||
|
||||
# Run from a neutral cwd with args; check the three output lines.
|
||||
# Run from a neutral cwd with args. The first line is an embedded resource
|
||||
# (deps.edn :jolt/build :embed), proving io/resource resolves from the binary with
|
||||
# no resources/ dir on disk; the rest exercise a macro, cross-ns, and args.
|
||||
got="$(cd / && "$out" alpha bb ccc 2>&1)"
|
||||
want='HELLO FROM A BUILT BINARY!
|
||||
want='embedded resource ok
|
||||
HELLO FROM A BUILT BINARY!
|
||||
HELLO FROM A BUILT BINARY!
|
||||
args: [alpha bb ccc]
|
||||
sum: 10'
|
||||
|
|
|
|||
|
|
@ -146,12 +146,72 @@
|
|||
;; The loop itself is emit-image's ei-emit-ns* (optimize? #t, guard? #f).
|
||||
(define (bld-emit-ns ns-name src) (ei-emit-ns* ns-name src #t #f))
|
||||
|
||||
;; --- bundling: native libs + resources --------------------------------------
|
||||
;; A jolt seq of jolt strings -> a Scheme list of Scheme strings.
|
||||
(define (bld-strs x) (map jolt-str-render-one (seq->list x)))
|
||||
|
||||
;; Emit native-library loads. `natives` is the encoded jolt seq jolt.main/
|
||||
;; encode-natives produced: each entry is ["process"] | ["req" cand…] | ["opt" cand…].
|
||||
;; `which` selects 'required (process + req) or 'optional. Required + process loads
|
||||
;; are emitted before the app forms (the app's defcfn foreign-procedures resolve
|
||||
;; their symbols at top-level eval during startup, so the libs must be loaded
|
||||
;; first); a load-shared-object failure there is fatal — correct for a required
|
||||
;; lib. Optional loads run in the scheme-start launcher, where guard catches a
|
||||
;; missing lib (an optional lib's namespace is only present when the app requires
|
||||
;; it, so its foreign-procedures aren't among the baked top-level forms).
|
||||
(define (bld-emit-natives out natives which)
|
||||
(for-each
|
||||
(lambda (entry)
|
||||
(let* ((parts (bld-strs entry)) (kind (car parts)) (cands (cdr parts))
|
||||
(cand-lits (fold-left (lambda (s c) (string-append s (ei-str-lit c) " ")) "" cands)))
|
||||
(cond
|
||||
((and (eq? which 'required) (string=? kind "process"))
|
||||
(put-string out "(jolt-build-load-native '() #f #t)\n"))
|
||||
((and (eq? which 'required) (string=? kind "req"))
|
||||
(put-string out (string-append "(jolt-build-load-native (list " cand-lits ") #f #f)\n")))
|
||||
((and (eq? which 'optional) (string=? kind "opt"))
|
||||
(put-string out (string-append "(jolt-build-load-native (list " cand-lits ") #t #f)\n"))))))
|
||||
(seq->list natives)))
|
||||
|
||||
;; Walk an embed root recursively; return (resource-name . abspath) pairs, where
|
||||
;; resource-name is the "/"-joined path under the root (what io/resource is asked for).
|
||||
(define (bld-walk-files root rel acc)
|
||||
(let ((dir (if (string=? rel "") root (string-append root "/" rel))))
|
||||
(fold-left
|
||||
(lambda (acc name)
|
||||
(let* ((relpath (if (string=? rel "") name (string-append rel "/" name)))
|
||||
(full (string-append root "/" relpath)))
|
||||
(if (file-directory? full)
|
||||
(bld-walk-files root relpath acc)
|
||||
(cons (cons relpath full) acc))))
|
||||
acc
|
||||
(directory-list dir))))
|
||||
|
||||
;; Emit register-embedded-resource! per file under each embed dir. Emitted BEFORE
|
||||
;; the app forms so the (read-file-string ABSPATH) runs at heap build — the file's
|
||||
;; contents bake into the boot image and io/resource serves them with no file on
|
||||
;; disk. ABSPATH only has to exist at build time.
|
||||
(define (bld-emit-embeds out embed-dirs)
|
||||
(for-each
|
||||
(lambda (root)
|
||||
(when (file-directory? root)
|
||||
(for-each
|
||||
(lambda (rp)
|
||||
(put-string out (string-append
|
||||
"(register-embedded-resource! " (ei-str-lit (car rp))
|
||||
" (read-file-string " (ei-str-lit (cdr rp)) "))\n")))
|
||||
(bld-walk-files root "" '()))))
|
||||
(bld-strs embed-dirs)))
|
||||
|
||||
;; --- the build --------------------------------------------------------------
|
||||
;; entry-ns: the app's main namespace (a string). out-path: the binary to write.
|
||||
;; mode: "dev" | "release" | "optimized". Every form runs through jolt.passes/
|
||||
;; run-passes (const-fold always; inline + type inference when optimized turns on
|
||||
;; direct-linking). Deps + source roots are already applied by the caller.
|
||||
(define (build-binary entry-ns out-path mode)
|
||||
;; natives: encoded :jolt/native libs to load at startup. embed-dirs: dirs whose
|
||||
;; files bake into the binary (single-file). ext-roots: project-relative io/resource
|
||||
;; roots resolved at runtime against JOLT_PWD (ship-alongside resources).
|
||||
(define (build-binary entry-ns out-path mode natives embed-dirs ext-roots)
|
||||
(bld-check-toolchain)
|
||||
;; 1. record app namespaces in dependency order as they finish loading.
|
||||
(let ((app-order '()))
|
||||
|
|
@ -180,16 +240,40 @@
|
|||
;; 3. flat source = runtime + app + launcher.
|
||||
(let ((out (open-output-file flat-ss 'replace)))
|
||||
(bld-emit-runtime out)
|
||||
;; Load native libs, bake embedded resources, and point source roots at
|
||||
;; the build-time app roots — all BEFORE the app forms. The app's
|
||||
;; top-level forms run at binary startup (Sbuild_heap), and they include
|
||||
;; foreign-procedure evals (a library's defcfn) and (slurp (io/resource …))
|
||||
;; reads. So the libraries must be loaded and resources resolvable by the
|
||||
;; time those forms run, not later in the scheme-start launcher.
|
||||
(put-string out "\n;; === native libraries (required) ===\n")
|
||||
(bld-emit-natives out natives 'required)
|
||||
(put-string out "\n;; === embedded resources ===\n")
|
||||
(bld-emit-embeds out embed-dirs)
|
||||
(put-string out (string-append
|
||||
"(set-source-roots! (list "
|
||||
(fold-left (lambda (s r) (string-append s (ei-str-lit r) " ")) ""
|
||||
(get-source-roots))
|
||||
"))\n"))
|
||||
(put-string out "\n;; === app ===\n")
|
||||
(for-each (lambda (s) (put-string out s) (put-string out "\n")) app-strs)
|
||||
;; The launcher runs as Chez's scheme-start (so argv reaches -main —
|
||||
;; top-level boot forms run during heap build, before args are set), and
|
||||
;; suppresses the interactive greeting.
|
||||
;; suppresses the interactive greeting. It resets source roots to the
|
||||
;; app's resource dirs resolved against JOLT_PWD (or cwd) so a runtime
|
||||
;; io/resource that wasn't embedded still resolves next to the binary.
|
||||
(put-string out "\n;; === launcher ===\n")
|
||||
(put-string out "(suppress-greeting #t)\n")
|
||||
(put-string out "(scheme-start\n (lambda args\n")
|
||||
(bld-emit-natives out natives 'optional)
|
||||
(put-string out (string-append
|
||||
" (let ((base (or (getenv \"JOLT_PWD\") \".\")))\n"
|
||||
" (set-source-roots!\n"
|
||||
" (append (map (lambda (r) (string-append base \"/\" r)) (list "
|
||||
(fold-left (lambda (s r) (string-append s (ei-str-lit r) " ")) "" (bld-strs ext-roots))
|
||||
"))\n"
|
||||
" (list \"jolt-core\" \"stdlib\"))))\n"))
|
||||
(put-string out (string-append
|
||||
"(suppress-greeting #t)\n"
|
||||
"(scheme-start\n"
|
||||
" (lambda args\n"
|
||||
" (let ((mainv (var-deref " (ei-str-lit entry-ns) " \"-main\")))\n"
|
||||
" (apply jolt-invoke mainv args))\n"
|
||||
" (exit 0)))\n"))
|
||||
|
|
@ -233,8 +317,9 @@
|
|||
(display (string-append "jolt build: wrote " out-path "\n"))))))
|
||||
|
||||
(def-var! "jolt.host" "build-binary"
|
||||
(lambda (entry out mode)
|
||||
(lambda (entry out mode natives embed-dirs ext-roots)
|
||||
(build-binary (jolt-str-render-one entry)
|
||||
(jolt-str-render-one out)
|
||||
(jolt-str-render-one mode))
|
||||
(jolt-str-render-one mode)
|
||||
natives embed-dirs ext-roots)
|
||||
jolt-nil))
|
||||
|
|
|
|||
|
|
@ -116,7 +116,44 @@
|
|||
(foreign-set! 'unsigned-8 p n 0)
|
||||
p))
|
||||
|
||||
;; --- callbacks: receive calls FROM C ----------------------------------------
|
||||
;; jolt.ffi/foreign-callable lowers to (jolt-ffi-register-callable! (foreign-callable …)).
|
||||
;; A foreign-callable code object must be LOCKED (so the collector neither moves
|
||||
;; nor reclaims it) and RETAINED while C may still call through its entry point.
|
||||
;; Register it keyed by that entry-point address (a jolt pointer integer) — which
|
||||
;; is what the caller hands to C; free-callable unlocks and drops it. A callback
|
||||
;; left registered lives for the process (the GTK-signal-handler common case).
|
||||
(define ffi-callable-table (make-eqv-hashtable)) ; entry-point addr -> code object
|
||||
(define (jolt-ffi-register-callable! co)
|
||||
(lock-object co)
|
||||
(let ((addr (foreign-callable-entry-point co)))
|
||||
(hashtable-set! ffi-callable-table addr co)
|
||||
addr))
|
||||
(define (ffi-free-callable addr)
|
||||
(let* ((a (jnum->exact addr)) (co (hashtable-ref ffi-callable-table a #f)))
|
||||
(when co (unlock-object co) (hashtable-delete! ffi-callable-table a))
|
||||
jolt-nil))
|
||||
|
||||
;; --- native libraries for a standalone binary -------------------------------
|
||||
;; `jolt build` bakes a project's deps.edn :jolt/native declarations into the
|
||||
;; launcher, which loads them at startup (load-shared-object isn't part of the
|
||||
;; saved heap, so it must run in the built process, not at heap build). process?
|
||||
;; loads the running binary's own symbols (libc sockets); otherwise try each
|
||||
;; platform candidate in turn and fail unless the spec is optional.
|
||||
(define (jolt-build-load-native cands optional? process?)
|
||||
(if process?
|
||||
(begin (load-shared-object #f) #t)
|
||||
(let loop ((cs cands))
|
||||
(cond
|
||||
((null? cs)
|
||||
(unless optional?
|
||||
(error 'jolt-build "required native library not found" cands))
|
||||
#f)
|
||||
((guard (e (#t #f)) (load-shared-object (car cs)) #t) #t)
|
||||
(else (loop (cdr cs)))))))
|
||||
|
||||
;; --- expose under jolt.ffi ---------------------------------------------------
|
||||
(def-var! "jolt.ffi" "free-callable" ffi-free-callable)
|
||||
(def-var! "jolt.ffi" "load-library" ffi-load-library)
|
||||
(def-var! "jolt.ffi" "loaded?" (lambda (n) (if (ffi-loaded? n) #t #f)))
|
||||
(def-var! "jolt.ffi" "alloc" ffi-alloc)
|
||||
|
|
|
|||
|
|
@ -19,6 +19,16 @@
|
|||
;; path string of any value: a jfile -> its path, else its str rendering.
|
||||
(define (file-path-of x) (if (jfile? x) (jfile-path x) (jolt-str-render-one x)))
|
||||
|
||||
;; Resources baked into a standalone binary by `jolt build` (deps.edn
|
||||
;; :jolt/build :embed). The build emits a register-embedded-resource! per file at
|
||||
;; heap-build time, so the contents live in the boot image — io/resource serves
|
||||
;; them with no file on disk. An embedded hit reads through slurp/reader exactly
|
||||
;; like a jfile would.
|
||||
(define embedded-resources (make-hashtable equal-hash equal?))
|
||||
(define (register-embedded-resource! name content)
|
||||
(hashtable-set! embedded-resources name content))
|
||||
(define-record-type embedded-res (fields name content) (nongenerative jolt-embres-v1))
|
||||
|
||||
;; A user-facing relative path resolves against JOLT_PWD — the user's cwd before
|
||||
;; the launcher cd'd to the jolt repo root — matching the JVM, where io/file is
|
||||
;; cwd-relative. (io/resource builds jfiles from the source roots directly, so it
|
||||
|
|
@ -191,6 +201,7 @@
|
|||
(define (jolt-slurp src . opts)
|
||||
(cond
|
||||
((jfile? src) (read-file-string (jfile-path src)))
|
||||
((embedded-res? src) (embedded-res-content src))
|
||||
((reader-jhost? src) (drain-reader src))
|
||||
;; bytes (a bytevector or a jolt byte-array): decode with :encoding (UTF-8
|
||||
;; default). clj-http-lite slurps response-body byte arrays.
|
||||
|
|
@ -286,6 +297,7 @@
|
|||
(cond
|
||||
((reader-jhost? x) x)
|
||||
((jfile? x) (host-new "StringReader" (read-file-string (jfile-path x))))
|
||||
((embedded-res? x) (host-new "StringReader" (embedded-res-content x)))
|
||||
((and (jhost? x) (string=? (jhost-tag x) "url"))
|
||||
(host-new "StringReader" (read-file-string (url-strip-scheme (url-spec x)))))
|
||||
((string? x) (host-new "StringReader" (read-file-string x)))
|
||||
|
|
@ -317,11 +329,13 @@
|
|||
;; (slurp/reader-able) for the first match, else nil. get-source-roots is the
|
||||
;; loader's accessor (loader.ss), resolved at call time — the runtime CLI loads it.
|
||||
(define (jolt-io-resource name)
|
||||
(let ((nm (jolt-str-render-one name)))
|
||||
(let loop ((roots (get-source-roots)))
|
||||
(cond ((null? roots) jolt-nil)
|
||||
((file-exists? (string-append (car roots) "/" nm)) (make-jfile (string-append (car roots) "/" nm)))
|
||||
(else (loop (cdr roots)))))))
|
||||
(let* ((nm (jolt-str-render-one name))
|
||||
(emb (hashtable-ref embedded-resources nm #f)))
|
||||
(if emb (make-embedded-res nm emb)
|
||||
(let loop ((roots (get-source-roots)))
|
||||
(cond ((null? roots) jolt-nil)
|
||||
((file-exists? (string-append (car roots) "/" nm)) (make-jfile (string-append (car roots) "/" nm)))
|
||||
(else (loop (cdr roots))))))))
|
||||
(def-var! "clojure.java.io" "resource" jolt-io-resource)
|
||||
;; as-url honors a library-registered URL class (e.g. jolt-lang/http-client's full
|
||||
;; java.net.URL shim) so io/as-url and (URL. spec) agree; else the file-only jhost.
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -424,6 +424,28 @@
|
|||
:rettype (name (nth items 3))
|
||||
:blocking (and (= 5 (count items)) (= "blocking" (name (nth items 4))))})
|
||||
|
||||
;; jolt.ffi/__ccallable: the foreign-CALLBACK form (via the jolt.ffi/foreign-callable
|
||||
;; macro) — the inverse of __cfn. It wraps a jolt fn as a C-callable function
|
||||
;; pointer so C can call back INTO jolt (GTK signal handlers, qsort comparators).
|
||||
;; Shape:
|
||||
;; (jolt.ffi/__ccallable f [:argtype ...] :rettype) ; thread stays active
|
||||
;; (jolt.ffi/__ccallable f [:argtype ...] :rettype :collect-safe) ; may be invoked
|
||||
;; ; while the thread is
|
||||
;; ; parked in a :blocking call
|
||||
;; Unlike __cfn, the fn is a CHILD expression (analyzed + walked by the passes);
|
||||
;; the types are literal keywords read at compile time. The Chez back end lowers
|
||||
;; it to a locked `foreign-callable` and returns its entry-point address (a jolt
|
||||
;; pointer). :collect-safe is required when C invokes the callback from a thread
|
||||
;; that is deactivated inside a :blocking foreign call (e.g. a GTK main loop).
|
||||
(defn- analyze-ffi-callable [ctx items env]
|
||||
(when-not (<= 4 (count items) 5)
|
||||
(throw (str "jolt.ffi/foreign-callable expects (foreign-callable f [argtypes] rettype [:collect-safe])")))
|
||||
{:op :ffi-callable
|
||||
:fn (analyze ctx (nth items 1) env)
|
||||
:argtypes (mapv name (form-vec-items (nth items 2)))
|
||||
:rettype (name (nth items 3))
|
||||
:collect-safe (and (= 5 (count items)) (= "collect-safe" (name (nth items 4))))})
|
||||
|
||||
;; The `.` special form: `(. target member arg*)` — member access / method call.
|
||||
;; A symbol member whose name starts with "-" is a field read; otherwise it is a
|
||||
;; method (call with the trailing args). Both lower to a :host-call carrying the
|
||||
|
|
@ -506,6 +528,11 @@
|
|||
(and (form-sym? head) (= "jolt.ffi" (form-sym-ns head))
|
||||
(= "__cfn" (form-sym-name head)))
|
||||
(analyze-ffi-fn ctx items env)
|
||||
;; jolt.ffi/__ccallable — the foreign-callback special form (the fn is a
|
||||
;; child expression, analyzed here).
|
||||
(and (form-sym? head) (= "jolt.ffi" (form-sym-ns head))
|
||||
(= "__ccallable" (form-sym-name head)))
|
||||
(analyze-ffi-callable ctx items env)
|
||||
;; special-form heads are NOT shadowable (unlike macros): a local named
|
||||
;; `if` does not change the meaning of (if …) in operator position, per
|
||||
;; spec §3 and the reference. No (not shadowed) guard here.
|
||||
|
|
|
|||
|
|
@ -287,6 +287,19 @@
|
|||
" (" (str/join " " (map ffi-type->chez (:argtypes node))) ") "
|
||||
(ffi-type->chez (:rettype node)) ")"))
|
||||
|
||||
;; jolt.ffi/__ccallable -> a Chez foreign-callable wrapping the emitted jolt fn,
|
||||
;; locked + registered (jolt-ffi-register-callable!, host/chez/ffi.ss) so the
|
||||
;; collector neither moves nor reclaims it while C may still call through it. The
|
||||
;; expression evaluates to the entry-point address — a jolt pointer the caller
|
||||
;; hands to C. :collect-safe emits the convention that reactivates the thread on
|
||||
;; entry, for callbacks invoked while it is parked in a :blocking foreign call.
|
||||
(defn- emit-ffi-callable [node]
|
||||
(str "(jolt-ffi-register-callable! (foreign-callable "
|
||||
(when (:collect-safe node) "__collect_safe ")
|
||||
(emit (:fn node))
|
||||
" (" (str/join " " (map ffi-type->chez (:argtypes node))) ") "
|
||||
(ffi-type->chez (:rettype node)) "))"))
|
||||
|
||||
(defn- emit-recur [node]
|
||||
(when-not *recur-target* (throw (ex-info "emit: recur outside a loop/fn target" {})))
|
||||
(let [arg-nodes (:args node)]
|
||||
|
|
@ -500,6 +513,7 @@
|
|||
:loop (emit-loop node)
|
||||
:recur (emit-recur node)
|
||||
:ffi-fn (emit-ffi-fn node)
|
||||
:ffi-callable (emit-ffi-callable node)
|
||||
:fn (emit-fn node)
|
||||
;; (def name) with no init (declare): reserve the cell. A def with non-empty
|
||||
;; reader metadata lowers to def-var-with-meta! (ported in a later increment).
|
||||
|
|
|
|||
|
|
@ -127,6 +127,14 @@
|
|||
{dep-roots :roots dep-natives :natives} (resolve-deps all-deps project-dir)]
|
||||
{:roots (vec (distinct (concat project-roots dep-roots)))
|
||||
:main-opts main-opts
|
||||
;; the project's own paths (relative to project-dir) and absolute resource
|
||||
;; roots, plus its :jolt/build options — `jolt build` uses these to bundle
|
||||
;; resources into / alongside a standalone binary.
|
||||
:project-dir project-dir
|
||||
:project-paths (vec project-paths)
|
||||
:project-roots (vec project-roots)
|
||||
:build (:jolt/build edn)
|
||||
:embed-dirs (mapv #(abspath project-dir %) (:embed (:jolt/build edn)))
|
||||
:tasks (:tasks edn)
|
||||
:natives (vec (distinct (concat (:jolt/native edn) dep-natives)))
|
||||
;; nREPL middleware a library contributes (jolt.nrepl composes them over its
|
||||
|
|
|
|||
|
|
@ -94,6 +94,7 @@
|
|||
(= op :set-var) (assoc node :val (f (get node :val)))
|
||||
(= op :set-field) (assoc node :obj (f (get node :obj)) :val (f (get node :val)))
|
||||
(= op :defmacro) (assoc node :fn (f (get node :fn)))
|
||||
(= op :ffi-callable) (assoc node :fn (f (get node :fn)))
|
||||
(= op :invoke) (assoc node :fn (f (get node :fn))
|
||||
:args (mapv f (get node :args)))
|
||||
(= op :vector) (assoc node :items (mapv f (get node :items)))
|
||||
|
|
@ -140,6 +141,7 @@
|
|||
(= op :set-var) (f acc (get node :val))
|
||||
(= op :set-field) (f (f acc (get node :obj)) (get node :val))
|
||||
(= op :defmacro) (f acc (get node :fn))
|
||||
(= op :ffi-callable) (f acc (get node :fn))
|
||||
(= op :invoke) (reduce f (f acc (get node :fn)) (get node :args))
|
||||
(= op :vector) (reduce f acc (get node :items))
|
||||
(= op :set) (reduce f acc (get node :items))
|
||||
|
|
|
|||
|
|
@ -8,6 +8,12 @@
|
|||
|
||||
(defn- project-dir [] (or (jolt.host/getenv "JOLT_PWD") "."))
|
||||
|
||||
(defn- current-platform []
|
||||
(let [os (str/lower-case (or (System/getProperty "os.name") ""))]
|
||||
(cond (str/includes? os "mac") :darwin
|
||||
(str/includes? os "win") :windows
|
||||
:else :linux)))
|
||||
|
||||
;; Load a library's declared native shared objects (deps.edn :jolt/native) before
|
||||
;; its Clojure is required, so its foreign-fn bindings resolve. Each entry is a
|
||||
;; map: {:name "sqlite3" :darwin ["libsqlite3.0.dylib" ...] :linux ["libsqlite3.so.0" ...]}
|
||||
|
|
@ -15,10 +21,7 @@
|
|||
;; (use the running process's symbols, e.g. libc sockets — no external file).
|
||||
(defn- load-natives! [natives]
|
||||
(when (seq natives)
|
||||
(let [os (str/lower-case (or (System/getProperty "os.name") ""))
|
||||
plat (cond (str/includes? os "mac") :darwin
|
||||
(str/includes? os "win") :windows
|
||||
:else :linux)]
|
||||
(let [plat (current-platform)]
|
||||
(doseq [spec natives]
|
||||
(if (:process spec)
|
||||
(jolt.ffi/load-library)
|
||||
|
|
@ -116,23 +119,41 @@
|
|||
;; standalone executable. Resolves deps + roots like `run`, then hands the entry
|
||||
;; namespace to the host build driver (jolt.host/build-binary, defined by
|
||||
;; build.ss). Default mode is release; --opt selects optimized, --dev unoptimized.
|
||||
;; Encode a deps.edn :jolt/native spec for the build launcher, resolving the
|
||||
;; current platform's candidate list now (the binary runs on this OS). Each entry
|
||||
;; becomes a vector the launcher (build.ss) reads: ["process"] for the running
|
||||
;; binary's own symbols, else ["req"|"opt" cand…] to try in turn.
|
||||
(defn- encode-natives [natives]
|
||||
(let [plat (current-platform)]
|
||||
(vec (for [spec natives]
|
||||
(if (:process spec)
|
||||
["process"]
|
||||
(let [c (get spec plat)
|
||||
cands (if (string? c) [c] (vec c))]
|
||||
(into [(if (:optional spec) "opt" "req")] cands)))))))
|
||||
|
||||
(defn- cmd-build [more]
|
||||
(apply-project! (deps/resolve-project (project-dir)))
|
||||
(let [opts (loop [a more, entry nil, out nil]
|
||||
(cond
|
||||
(empty? a) {:entry entry :out out}
|
||||
(= "-m" (first a)) (recur (drop 2 a) (second a) out)
|
||||
(= "-o" (first a)) (recur (drop 2 a) entry (second a))
|
||||
(str/starts-with? (first a) "-") (recur (rest a) entry out)
|
||||
:else (recur (rest a) (or entry (first a)) out)))
|
||||
entry (:entry opts)
|
||||
mode (cond (some #{"--opt"} more) "optimized"
|
||||
(some #{"--dev"} more) "dev"
|
||||
:else "release")]
|
||||
(when (nil? entry)
|
||||
(throw (ex-info "build needs an entry: -m NS" {})))
|
||||
(let [out (or (:out opts) (first (str/split entry #"\.")))]
|
||||
(jolt.host/build-binary entry out mode))))
|
||||
(let [{:keys [project-paths embed-dirs] :as resolved}
|
||||
(deps/resolve-project (project-dir))]
|
||||
(apply-project! resolved)
|
||||
(let [opts (loop [a more, entry nil, out nil]
|
||||
(cond
|
||||
(empty? a) {:entry entry :out out}
|
||||
(= "-m" (first a)) (recur (drop 2 a) (second a) out)
|
||||
(= "-o" (first a)) (recur (drop 2 a) entry (second a))
|
||||
(str/starts-with? (first a) "-") (recur (rest a) entry out)
|
||||
:else (recur (rest a) (or entry (first a)) out)))
|
||||
entry (:entry opts)
|
||||
mode (cond (some #{"--opt"} more) "optimized"
|
||||
(some #{"--dev"} more) "dev"
|
||||
:else "release")]
|
||||
(when (nil? entry)
|
||||
(throw (ex-info "build needs an entry: -m NS" {})))
|
||||
(let [out (or (:out opts) (first (str/split entry #"\.")))
|
||||
natives (encode-natives (:natives resolved))]
|
||||
;; embed-dirs (absolute) are walked + baked into the binary by the driver;
|
||||
;; project-paths (relative) become runtime io/resource roots (ship-alongside).
|
||||
(jolt.host/build-binary entry out mode natives embed-dirs project-paths)))))
|
||||
|
||||
(defn- nrepl [more]
|
||||
;; resolve the project (deps on the roots, native libs loaded), then start the
|
||||
|
|
|
|||
|
|
@ -16,7 +16,9 @@
|
|||
|
||||
The memory/library primitives (alloc/free/read/write/sizeof/load-library/
|
||||
ptr->string/string->ptr/null/null?) are provided by the host. foreign-fn lowers
|
||||
a compile-time-typed signature to a real Chez foreign-procedure.")
|
||||
a compile-time-typed signature to a real Chez foreign-procedure. foreign-callable
|
||||
is the inverse — it wraps a jolt fn as a C-callable function pointer so C can
|
||||
call back into jolt (e.g. GTK signal handlers); free-callable releases it.")
|
||||
|
||||
;; foreign-fn binds C symbol `csym` to a typed callable. Expands to the __cfn
|
||||
;; special form (always fully-qualified, so an :as alias on jolt.ffi resolves):
|
||||
|
|
@ -33,3 +35,19 @@
|
|||
(list 'def name (if (= opt :blocking)
|
||||
(list 'jolt.ffi/__cfn csym argtypes rettype :blocking)
|
||||
(list 'jolt.ffi/__cfn csym argtypes rettype))))
|
||||
|
||||
;; foreign-callable wraps a jolt fn `f` as a C-callable function pointer — the
|
||||
;; inverse of foreign-fn, so C can call back INTO jolt (GTK signal handlers, a
|
||||
;; qsort comparator, any C API that takes a callback). Returns the pointer; pass
|
||||
;; it where C expects a function pointer. argtypes/rettype use the same keywords
|
||||
;; as foreign-fn; the args C passes arrive as jolt values and the jolt return is
|
||||
;; marshaled back. The callback stays live until free-callable is called on the
|
||||
;; pointer. Pass a trailing :collect-safe when C invokes the callback from a
|
||||
;; thread parked in a :blocking foreign call (e.g. a GTK main loop):
|
||||
;; (g-signal-connect button "clicked"
|
||||
;; (ffi/foreign-callable on-click [:pointer :pointer] :void :collect-safe)
|
||||
;; (ffi/null))
|
||||
(defmacro foreign-callable [f argtypes rettype & [opt]]
|
||||
(if (= opt :collect-safe)
|
||||
(list 'jolt.ffi/__ccallable f argtypes rettype :collect-safe)
|
||||
(list 'jolt.ffi/__ccallable f argtypes rettype)))
|
||||
|
|
|
|||
|
|
@ -1 +1,8 @@
|
|||
{:paths ["src"]}
|
||||
{:paths ["src" "resources"]
|
||||
|
||||
;; exercise the build's native-lib loading: a :process spec loads the running
|
||||
;; binary's own symbols (libc) at startup — no external file, always succeeds.
|
||||
:jolt/native [{:name "libc" :process true}]
|
||||
|
||||
;; bake resources/ into the binary so io/resource resolves with no file on disk.
|
||||
:jolt/build {:embed ["resources"]}}
|
||||
|
|
|
|||
1
test/chez/build-app/resources/greeting.txt
Normal file
1
test/chez/build-app/resources/greeting.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
embedded resource ok
|
||||
|
|
@ -1,7 +1,11 @@
|
|||
(ns app.core
|
||||
(:require [app.util :as util]))
|
||||
(:require [app.util :as util]
|
||||
[clojure.java.io :as io]))
|
||||
|
||||
(defn -main [& args]
|
||||
;; the resource is baked into the binary (deps.edn :jolt/build :embed), so this
|
||||
;; resolves with no resources/ dir on disk, run from any cwd.
|
||||
(println (slurp (io/resource "greeting.txt")))
|
||||
(util/twice (println (util/shout "hello from a built binary")))
|
||||
(println "args:" (vec args))
|
||||
(println "sum:" (reduce + (map count args))))
|
||||
|
|
|
|||
|
|
@ -61,5 +61,29 @@
|
|||
(let loop ((i 0)) (when (fx<? i 30000000) (loop (fx+ i 1)))) ; spin so the thread enters usleep
|
||||
(ok "blocking ffi call is collect-safe" (guard (e (#t #f)) (collect) #t)))
|
||||
|
||||
;; callbacks: receive a call FROM C. A foreign-callable wraps a jolt fn as a
|
||||
;; C-callable function pointer (what GTK signal handlers / qsort comparators need).
|
||||
;; Build a comparator and sort an int array through libc qsort.
|
||||
(ev "(def cmp (jolt.ffi/__ccallable
|
||||
(fn [pa pb]
|
||||
(let [a (jolt.ffi/read pa :int) b (jolt.ffi/read pb :int)]
|
||||
(cond (< a b) -1 (> a b) 1 :else 0)))
|
||||
[:pointer :pointer] :int))")
|
||||
(ok "foreign-callable returns a pointer"
|
||||
(let ((p (jnum->exact (var-deref "user" "cmp")))) (and (integer? p) (> p 0))))
|
||||
(ev "(def c-qsort (jolt.ffi/__cfn \"qsort\" [:pointer :size_t :size_t :pointer] :void))")
|
||||
(ok "C calls back into jolt: qsort with a jolt comparator"
|
||||
(jolt-truthy?
|
||||
(ev "(let [n 5 w (jolt.ffi/sizeof :int) p (jolt.ffi/alloc (* n w))]
|
||||
(doseq [[i v] (map vector (range n) [3 1 4 1 5])]
|
||||
(jolt.ffi/write p :int (* i w) v))
|
||||
(c-qsort p n w cmp)
|
||||
(let [out (mapv (fn [i] (jolt.ffi/read p :int (* i w))) (range n))]
|
||||
(jolt.ffi/free p)
|
||||
(= out [1 1 3 4 5])))")))
|
||||
;; free-callable unlocks + drops the code object, returning nil.
|
||||
(ok "free-callable releases the callback"
|
||||
(jolt-nil? (ev "(jolt.ffi/free-callable cmp)")))
|
||||
|
||||
(printf "~a/~a passed~n" (- total fails) total)
|
||||
(exit (if (zero? fails) 0 1))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue