diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bcf5dd5..c263a4e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/.gitignore b/.gitignore index 2e37fdb..cf71c3c 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ node_modules/ cljs-test-runner-out/ target/native/ bin/ +.squint-out/ diff --git a/README.md b/README.md index bc0f888..9ffa70c 100644 --- a/README.md +++ b/README.md @@ -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, diff --git a/bb.edn b/bb.edn index 011b716..9ab5ca4 100644 --- a/bb.edn +++ b/bb.edn @@ -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"] @@ -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 @@ -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}}} diff --git a/package-lock.json b/package-lock.json index 1e0a8f7..8a2a144 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,7 +6,8 @@ "": { "devDependencies": { "nbb": "1.5.212", - "shadow-cljs": "3.5.3" + "shadow-cljs": "3.5.3", + "squint-cljs": "^0.14.208" } }, "node_modules/base64-js": { @@ -62,6 +63,22 @@ "dev": true, "license": "MIT" }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -128,6 +145,20 @@ "node": ">= 0.6.0" } }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/readline-sync": { "version": "1.4.10", "resolved": "https://registry.npmjs.org/readline-sync/-/readline-sync-1.4.10.tgz", @@ -188,6 +219,24 @@ "source-map": "^0.6.0" } }, + "node_modules/squint-cljs": { + "version": "0.14.208", + "resolved": "https://registry.npmjs.org/squint-cljs/-/squint-cljs-0.14.208.tgz", + "integrity": "sha512-B5Sorb04wwA0bM32V8rgO3VhJL8icIKHt30Md41Ei3mim5mejd4HE6zX9o+uOxk9fqEiTJEoOgvPL0vF6WymrA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/borkdude" + } + ], + "dependencies": { + "chokidar": "^4.0.3" + }, + "bin": { + "squint": "node_cli.js" + } + }, "node_modules/which": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", diff --git a/package.json b/package.json index 759a265..e7d5fe9 100644 --- a/package.json +++ b/package.json @@ -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" } } diff --git a/squint.edn b/squint.edn new file mode 100644 index 0000000..520e8f6 --- /dev/null +++ b/squint.edn @@ -0,0 +1,3 @@ +{:paths ["src" "test-node"] + :output-dir ".squint-out" + :extension "mjs"} diff --git a/src/babashka/ffi.cljs b/src/babashka/ffi.cljs index 8d8a14b..fecb26a 100644 --- a/src/babashka/ffi.cljs +++ b/src/babashka/ffi.cljs @@ -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 @@ -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] @@ -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) @@ -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) @@ -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)))) @@ -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." @@ -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: @@ -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] @@ -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. diff --git a/test-node/babashka/ffi_test.cljs b/test-node/babashka/ffi_test.cljs index 60438f4..b943f78 100644 --- a/test-node/babashka/ffi_test.cljs +++ b/test-node/babashka/ffi_test.cljs @@ -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)) @@ -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 diff --git a/test-node/babashka/run_tests.cljs b/test-node/babashka/run_tests.cljs new file mode 100644 index 0000000..94ebc44 --- /dev/null +++ b/test-node/babashka/run_tests.cljs @@ -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)