Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,16 @@ jobs:
if: runner.os == 'Windows'
uses: ilammy/msvc-dev-cmd@v1

# gitlibs for the test runner, so a repository hiccup cannot fail a run twice
- name: Cache dependencies
uses: actions/cache@v4
with:
path: |
~/.m2/repository
~/.gitlibs
key: deps-${{ runner.os }}-${{ hashFiles('deps.edn', 'bb.edn') }}
restore-keys: deps-${{ runner.os }}-

# The download from objects.githubusercontent.com fails now and then
# with curl exit 8, a reply curl cannot parse. Retrying clears it.
- name: Install babashka dev build
Expand Down Expand Up @@ -61,6 +71,16 @@ jobs:
distribution: temurin
java-version: ${{ matrix.jdk }}

# Maven and gitlibs, so a repository hiccup cannot fail a run twice
- name: Cache dependencies
uses: actions/cache@v4
with:
path: |
~/.m2/repository
~/.gitlibs
key: deps-${{ runner.os }}-${{ hashFiles('deps.edn', 'bb.edn') }}
restore-keys: deps-${{ runner.os }}-

- uses: DeLaGuardo/setup-clojure@13.4
with:
cli: latest
Expand Down Expand Up @@ -116,6 +136,16 @@ jobs:
distribution: graalvm
github-token: ${{ secrets.GITHUB_TOKEN }}

# Maven and gitlibs, so a repository hiccup cannot fail a run twice
- name: Cache dependencies
uses: actions/cache@v4
with:
path: |
~/.m2/repository
~/.gitlibs
key: deps-${{ runner.os }}-${{ hashFiles('deps.edn', 'bb.edn') }}
restore-keys: deps-${{ runner.os }}-

- uses: DeLaGuardo/setup-clojure@13.4
with:
cli: latest
Expand Down Expand Up @@ -162,6 +192,16 @@ jobs:
distribution: temurin
java-version: '25'

# Maven and gitlibs, so a repository hiccup cannot fail a run twice
- name: Cache dependencies
uses: actions/cache@v4
with:
path: |
~/.m2/repository
~/.gitlibs
key: deps-${{ runner.os }}-${{ hashFiles('deps.edn', 'bb.edn') }}
restore-keys: deps-${{ runner.os }}-

- uses: DeLaGuardo/setup-clojure@13.4
with:
cli: latest
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ node_modules/
cljs-test-runner-out/
target/native/
bin/
.squint-out/
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# babashka.ffi

[![Clojars Project](https://img.shields.io/clojars/v/org.babashka/ffi.svg)](https://clojars.org/org.babashka/ffi)
[![bb built-in](https://raw.githubusercontent.com/babashka/babashka/master/logo/built-in-badge.svg)](https://book.babashka.org#badges)

Call C libraries from Clojure and [babashka](https://github.com/babashka/babashka).

This library is the `babashka.ffi` namespace that ships inside babashka,
Expand Down
19 changes: 14 additions & 5 deletions bb.edn
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
;; Only quickdoc and publish need a dependency, and they carry their own:
;; every other task then starts without resolving a classpath, which is what
;; the test jobs do on CI.
{:paths ["src" "bb"]
:deps {io.github.borkdude/quickdoc
#_{:local/root "/Users/borkdude/dev/quickdoc"}
{:git/sha "5097746c91fbd6605792e7dc817a81d20b8d691a"}
;; tools.build itself is built into babashka, so build.clj runs here
io.github.babashka/deps-deploy {:mvn/version "0.0.1"}}
:tasks
{test:bb {:doc "Run the tests through babashka's built-in babashka.ffi."
:extra-paths ["test"]
Expand Down Expand Up @@ -35,8 +33,17 @@
(shell "npx" "shadow-cljs" "compile" "node-test" "node-test-advanced")
(shell "node" "target/shadow/node-tests.js")
(shell "node" "target/shadow/node-tests-advanced.js"))}
test:squint
{:doc "Compile the Node.js tests with squint and run them. Needs Node.js 26.1 or newer."
:task (do (shell "npm" "install")
(shell "npx" "squint" "compile")
(shell "node" ".squint-out/babashka/run_tests.mjs"))}

quickdoc
{:doc "Invoke quickdoc"
:extra-deps {io.github.borkdude/quickdoc
#_{:local/root "/Users/borkdude/dev/quickdoc"}
{:git/sha "5097746c91fbd6605792e7dc817a81d20b8d691a"}}
:requires ([quickdoc.api :as api])
;; the task body is evaluated, so the override map is quoted. The vars
;; a deftype generates are not API: the constructor is private at run
Expand Down Expand Up @@ -74,5 +81,7 @@

publish
{:doc "Build the jar and deploy it to Clojars. Pass --bump to bump the release counter first"
;; tools.build itself is built into babashka, so build.clj runs here
:extra-deps {io.github.babashka/deps-deploy {:mvn/version "0.0.1"}}
:extra-paths ["."]
:exec-fn tasks/publish}}}
51 changes: 50 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"private": true,
"devDependencies": {
"nbb": "1.5.212",
"shadow-cljs": "3.5.3"
"shadow-cljs": "3.5.3",
"squint-cljs": "^0.14.208"
}
}
3 changes: 3 additions & 0 deletions squint.edn
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{:paths ["src" "test-node"]
:output-dir ".squint-out"
:extension "mjs"}
18 changes: 16 additions & 2 deletions src/babashka/ffi.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
- a struct by value in a signature
- a variadic signature, :&
- a function pointer as the symbol. Bind a function by name."
{:squint/compile-time true}
(:refer-clojure :exclude [clone])
;; The ClojureScript compiler takes defcfn and with-open from ffi.clj, so
;; its JVM needs JDK 25 or newer. nbb uses the defmacros in this file when
Expand Down Expand Up @@ -73,6 +74,11 @@
(defn- bigint? [x]
(identical? js/BigInt (type x)))

(defn- digits
"The number s spells, or nil. squint has no parse-long."
[s]
(when (re-matches #"[0-9]+" s) (js/parseInt s 10)))

(defn- from-big
"Returns a number for a safe integer, otherwise the bigint."
[b]
Expand Down Expand Up @@ -412,7 +418,7 @@
(let [;; newest soname first, numerically:
;; libz.so.10 beats libz.so.9
vkey (fn [f]
(mapv #(or (parse-long %) -1)
(mapv #(or (digits %) -1)
(rest (str/split (subs f (count base)) #"\."))))
newest-first (fn [x y]
(let [a (vkey x) b (vkey y)
Expand Down Expand Up @@ -448,7 +454,9 @@
[lib]
(let [lib (cond (map? lib) lib
(fn? lib) (lib)
(or (delay? lib) (var? lib) (instance? Atom lib)) @lib
;; a delay, an atom or a var. squint has no Atom symbol to
;; name, and IDeref covers the three on both hosts
(satisfies? IDeref lib) @lib
:else lib)
lookup (when (map? lib) (:lookup lib))]
(if (instance? (.-DynamicLibrary nffi) lookup)
Expand Down Expand Up @@ -477,8 +485,10 @@

;; -- foreign functions --------------------------------------------------------

^{:squint/compile-time :both}
(def ^:private layout-kinds #{:struct :array :union})

^{:squint/compile-time :both}
(defn- layout-vector? [t]
(and (vector? t) (contains? layout-kinds (first t))))

Expand Down Expand Up @@ -578,6 +588,7 @@
general)
{:babashka.ffi/backend :node}))))

^{:squint/compile-time :both}
(defn ^:no-doc defcfn-form
"The form defcfn expands to. A function, so a compiled build has it: see
the ns form."
Expand Down Expand Up @@ -632,6 +643,7 @@
(fn ~name ~@fn-tail)))
`(def ~name ~binding-form)))))

^{:squint/compile-time true}
(defmacro defcfn
"Defines name as a C function binding created by cfn:

Expand Down Expand Up @@ -727,6 +739,7 @@
[]
@the-global-arena)

^{:squint/compile-time :both}
(defn ^:no-doc with-open-form
"The form with-open expands to. A function, so a compiled build has it."
[bindings body]
Expand All @@ -737,6 +750,7 @@
(babashka.ffi/with-open ~(subvec bindings 2) ~@body)
(finally (.close ~(nth bindings 0)))))))

^{:squint/compile-time true}
(defmacro with-open
"Evaluates body with each name bound to its value. Calls .close on each
value in reverse order when body returns or throws.
Expand Down
8 changes: 4 additions & 4 deletions test-node/babashka/ffi_test.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
`bb test:node`. The cases follow test/babashka/ffi_test.clj, without the
struct-by-value, variadic and function-pointer cases that node:ffi does
not support."
(:require [babashka.ffi :as ffi :refer [defcfn]]
(:require [babashka.ffi :as ffi]
[cljs.test :as t :refer [deftest is testing]]))

(defcfn strlen "strlen" [:string] :long)
(ffi/defcfn strlen "strlen" [:string] :long)

(def windows? (= "win32" js/process.platform))

Expand Down Expand Up @@ -490,8 +490,8 @@
(is (thrown-with-msg? js/Error #"at \[:msgs 1 :msg\], a :int field cannot take"
(ffi/write q (ffi/place outer [:msgs 1 :msg]) "x"))))))))

(defcfn c-abs "The absolute value." "abs" [:int] :int)
(defcfn twice-abs "abs" [:int] :int
(ffi/defcfn c-abs "The absolute value." "abs" [:int] :int)
(ffi/defcfn twice-abs "abs" [:int] :int
raw [x] (* 2 (raw x)))

(deftest macro-form-test
Expand Down
17 changes: 17 additions & 0 deletions test-node/babashka/run_tests.cljs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
(ns babashka.run-tests
"The squint entry point. squint has no test runner of its own, so this
namespace runs the shared Node.js suite and sets the exit code."
(:require [babashka.ffi-test]
[clojure.test :as t]))

(def ^:private old-fail (get-method t/report [:cljs.test/default :fail]))
(defmethod t/report [:cljs.test/default :fail] [m]
(set! (.-exitCode js/process) 1)
(old-fail m))

(def ^:private old-error (get-method t/report [:cljs.test/default :error]))
(defmethod t/report [:cljs.test/default :error] [m]
(set! (.-exitCode js/process) 1)
(old-error m))

(t/run-tests 'babashka.ffi-test)
Loading