{"id":51221304,"url":"https://github.com/leifericf/clj-zig","last_synced_at":"2026-06-28T07:31:03.586Z","repository":{"id":365780142,"uuid":"1271009748","full_name":"leifericf/clj-zig","owner":"leifericf","description":"Interactive Zig as a Clojure library.","archived":false,"fork":false,"pushed_at":"2026-06-26T12:26:07.000Z","size":547,"stargazers_count":11,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-06-26T14:12:53.485Z","etag":null,"topics":["clojure","ffi","jvm","panama","systems-programming","zig"],"latest_commit_sha":null,"homepage":"","language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/leifericf.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-16T08:48:49.000Z","updated_at":"2026-06-26T12:26:11.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/leifericf/clj-zig","commit_stats":null,"previous_names":["leifericf/clj-zig"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/leifericf/clj-zig","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/leifericf%2Fclj-zig","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/leifericf%2Fclj-zig/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/leifericf%2Fclj-zig/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/leifericf%2Fclj-zig/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/leifericf","download_url":"https://codeload.github.com/leifericf/clj-zig/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/leifericf%2Fclj-zig/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34881384,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-28T02:00:05.809Z","response_time":54,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["clojure","ffi","jvm","panama","systems-programming","zig"],"created_at":"2026-06-28T07:31:02.051Z","updated_at":"2026-06-28T07:31:03.579Z","avatar_url":"https://github.com/leifericf.png","language":"Clojure","funding_links":[],"categories":[],"sub_categories":[],"readme":"# clj-zig\n\n[![CI](https://github.com/leifericf/clj-zig/actions/workflows/ci.yml/badge.svg)](https://github.com/leifericf/clj-zig/actions/workflows/ci.yml)\n![Zig 0.16](https://img.shields.io/badge/Zig-0.16-f7a41d?logo=zig\u0026logoColor=white)\n![Java 22+](https://img.shields.io/badge/Java-22%2B-007396?logo=openjdk\u0026logoColor=white)\n\n**clj-zig is an experiment, a proof of concept.** It begins with a question: what would it look and feel like to bring systems-level programming into Clojure, and can that be done while keeping the data-oriented, REPL-driven workflow Clojure developers work in? Can a Clojure developer reach for Zig where native code is the right tool, in a form that stays idiomatic Clojure, without restricting what the native side can do?\n\nThis project explores how far those questions can be carried.\n\nStay in the REPL, define a function in a familiar shape, and drop into Zig only where native performance, explicit layout, comptime, or low-level control earns its keep.\n\nThe name is descriptive rather than clever: `clj` for Clojure, `zig` for the Zig that backs each function.\n\n## Core idea\n\n```clojure\n(defnz add\n  \"Adds two signed integers.\"\n  [x :i64\n   y :i64\n   :ret :i64]\n  \"return x + y;\")\n\n(add 20 22)\n;; =\u003e 42\n```\n\nA normal Clojure function. The body is real Zig. The signature vector is a Clojure data contract describing the boundary.\n\n## A wider tour\n\nThe signature vector is the whole contract. Widen it one type at a time.\n\n```clojure\n;; A slice arrives as a primitive array; :const makes it read-only.\n(defnz sum\n  [xs [:slice :const :f64]\n   :ret :f64]\n  \"var t: f64 = 0; for (xs) |x| t += x; return t;\")\n\n(sum (double-array [1.0 2.0 3.0]))\n;; =\u003e 6.0\n```\n\nNamed boundary types cross by value. A `defrecordz` returns a Clojure record:\n\n```clojure\n(defrecordz Point [x :f64 y :f64])\n\n(defnz midpoint\n  [a Point\n   b Point\n   :ret Point]\n  \"return .{ .x = (a.x + b.x) / 2.0, .y = (a.y + b.y) / 2.0 };\")\n\n(midpoint (-\u003ePoint 0.0 0.0) (-\u003ePoint 4.0 6.0))\n;; =\u003e #user.Point{:x 2.0, :y 3.0}\n```\n\nA `defenumz` member bridges to a keyword. clj-zig copies an `[:owned [:slice T]]` return into a vector and frees the native memory; a `[:handle T]` is an opaque native resource the caller frees. Errors cross as data and allocations stay explicit. The [Boundary Contract](docs/03-boundary-contract.md) lists the full type vocabulary.\n\n## Bigger bodies and C interop\n\nA body can also live in a real `.zig` file instead of a string. The file is ordinary Zig with full editor and `zig fmt` support; the generated wrapper calls its `pub fn`. The descriptor can link C libraries too, so a body may `@cImport` a C header directly:\n\n```clojure\n(defnz hypotenuse\n  [a :f64 b :f64 :ret :f64]\n  {:zig/file \"hyp.zig\" :c/link [\"m\"]})\n```\n\n```zig\n// hyp.zig\nconst c = @cImport({ @cInclude(\"math.h\"); });\npub fn hypotenuse(a: f64, b: f64) f64 {\n    return c.sqrt(a * a + b * b);\n}\n```\n\nThe file path resolves next to the source file, then on the classpath. See [ADR 26](docs/adr/26-external-zig-source-files.md) and [ADR 27](docs/adr/27-compile-options-c-interop.md).\n\n## Binding a prebuilt library\n\nA program also reaches libraries it did not compile and that have no Zig body to wrap: the platform's windowing or input library, a system framework, libc, the graphics loader. `clj-zig.foreign` is a small foreign-function toolkit for binding one directly: open it, describe a signature as data, bind a cached downcall, optionally hand native code a Clojure callback.\n\n```clojure\n(require '[clj-zig.foreign :as foreign])\n\n(let [lib (foreign/library-lookup\n           (foreign/resolve-library {:env       [\"LIBFOO\"]\n                                     :candidates [\"/opt/homebrew/lib/libfoo.dylib\"]\n                                     :default   \"/opt/homebrew/lib/libfoo.dylib\"}))\n      add (foreign/downcall lib \"foo_add\" foreign/c-int [foreign/c-int foreign/c-int])]\n  (foreign/call add (int 20) (int 22)))      ;; =\u003e 42\n```\n\n`downcall` caches its handle per distinct signature, so a per-frame caller invokes the cached handle directly and does no linker work; `upcall-stub` adapts a Clojure fn to a C function pointer for callback-driven APIs; `read-utf8-bounded` reads a NUL-terminated string out of untrusted foreign memory under a hard cap. See [ADR 37](docs/adr/37-foreign-function-toolkit.md) and [ADR 38](docs/adr/38-synchronous-upcall-stubs.md).\n\n## A namespace of native functions\n\nA Clojure namespace is a Zig namespace. Name a function without a body and clj-zig takes the body from the `pub fn` of the same name in the `.zig` file beside the namespace's source: `app/geometry.clj` pairs with `app/geometry.zig`. Shared imports, helpers, and types live once in that file, and `zig-deps` declares the namespace's C link flags so each function inherits them:\n\n```clojure\n(ns geometry\n  (:require [clj-zig.core :refer [defnz zig-deps]]))\n\n(zig-deps {:c/link [\"m\"]})         ;; link libm for the whole namespace\n\n(defnz hypotenuse)                 ;; signature and body from geometry.zig's pub fn hypotenuse\n(defnz circle-area)\n```\n\n```zig\n//! clj-zig: geometry\nconst c = @cImport({ @cInclude(\"math.h\"); });\nfn square(x: f64) f64 { return x * x; }\n\npub fn hypotenuse(a: f64, b: f64) f64 { return c.sqrt(square(a) + square(b)); }\npub fn circle_area(r: f64) f64 { return 3.141592653589793 * square(r); }\n```\n\nWith no signature, the boundary contract is inferred from the `pub fn` prototype: `pub fn hypotenuse(a: f64, b: f64) f64` gives `[a :f64 b :f64 :ret :f64]`. A Zig type fixes the shape, but a returned `[]T` or `*T` carries no ownership or handle policy in its type. A function returning one needs an explicit signature declaring `[:owned ...]` or `[:handle ...]`; until then it reports `:clj-zig/contract-policy-needed`.\n\nEach function still compiles to its own content-addressed library, so redefining one recompiles only that one and a failed compile keeps the last good binding. A kebab-case name maps to its snake_case `pub fn` (`circle-area` to `circle_area`); the optional `//! clj-zig: \u003cns\u003e` header asserts the file belongs to the namespace. A body file may `@import` sibling and subdirectory `.zig` files, which are reproduced and compiled alongside it. See [ADR 28](docs/adr/28-namespace-as-zig-namespace.md) and [ADR 29](docs/adr/29-multi-file-zig-imports.md).\n\n## Distributing a library\n\nA library built with clj-zig ships its native code precompiled, so a\nconsumer adds the dependency and calls functions with no Zig toolchain and\nno build step. At release, `clj-zig.bake/bake!` cross-compiles each `defnz`\nin a namespace for the target matrix into the resource tree the jar\ncarries:\n\n```clojure\n(clj-zig.bake/bake! {:ns 'com.example.widgets/native :out \"resources\"})\n```\n\nAt load, a consumer's `defnz` resolves its baked library from the classpath\nby target, namespace, name, and content hash, extracts it into the cache,\nand binds it without invoking Zig. The hash uses the pinned Zig version, so\nthe consumer reproduces the hash the author baked under without a toolchain;\na platform the author did not bake is a clean miss, compiled locally when a\ntoolchain is present. The default matrix is seven targets across Linux,\nmacOS, and Windows; a function linking a third-party C library is baked for\nthe host only. `examples/build.clj` shows bake, jar, and deploy. See\n[ADR 31](docs/adr/31-distribute-precompiled-artifacts.md) and the\n[Installation and Distribution](docs/09-installation-and-distribution.md)\nguide.\n\n## Inspect and redefine\n\nEvery function is an ordinary Var carrying its spec, source, and build status:\n\n```clojure\n(zig/spec #'sum)               ;; the normalized boundary contract, as data\n(zig/generated-source #'sum)   ;; the full Zig wrapper\n(zig/source #'sum)             ;; the body you wrote\n```\n\nRedefine like any `defn`, and a fresh library compiles. When a new body fails to compile, the diagnostic prints and the last good binding stays callable.\n\n## Pipeline\n\n```text\nClojure form\n  -\u003e  signature data\n  -\u003e  normalized boundary contract\n  -\u003e  generated Zig wrapper\n  -\u003e  Zig compilation\n  -\u003e  native library loading\n  -\u003e  ordinary Clojure Var\n```\n\n## Examples\n\nThe [`examples/`](examples/) directory holds small, runnable programs. Load one\nin a REPL and evaluate its `(comment ...)` block. The basics cover each boundary\ntype one file at a time. Four go further, into work that is hard or impossible\nfrom the JVM:\n\n- [`simd.clj`](examples/simd.clj): explicit SIMD over `@Vector` registers.\n- [`memory_layout.clj`](examples/memory_layout.clj): a packed native buffer mutated in place, no allocation, no GC.\n- [`bit_ops.clj`](examples/bit_ops.clj): sub-byte packing and single-instruction bit intrinsics.\n- [`inline_asm.clj`](examples/inline_asm.clj): inline assembly, with the bodies in sibling `.zig` files.\n\nAnd two show a namespace backed by a co-located `.zig`:\n\n- [`cinterop.clj`](examples/cinterop.clj): imports a C header with `@cImport` and links a C library, its body in a sibling `.zig` file.\n- [`geometry.clj`](examples/geometry.clj): bodyless functions sourced from the co-located `geometry.zig`, with `zig-deps` linking libm for the whole namespace.\n- [`multifile.clj`](examples/multifile.clj): a body split across two `.zig` files, the first `@import`ing the second.\n\n## Reading order\n\n1. [Vision Brief](docs/01-vision-brief.md): what clj-zig is, who it serves, what counts as success.\n2. [Interface Design](docs/02-interface-design.md): `defnz` and the family of z-suffixed forms.\n3. [Boundary Contract](docs/03-boundary-contract.md): how values cross and the type vocabulary.\n4. [REPL and Execution Model](docs/04-repl-and-execution-model.md): redefinition, caching, diagnostics.\n5. [Composability and Builders](docs/05-composability-and-builders.md): data-level reuse and macros.\n6. [Proof-of-Concept Plan](docs/06-proof-of-concept-plan.md): scope, phases, acceptance tests.\n7. [Design Principles and Decisions](docs/07-design-principles-and-decisions.md): the principles; decisions are ADRs in [docs/adr/](docs/adr/README.md).\n8. [Test Strategy](docs/08-test-strategy.md): how generative and exhaustive testing prove the boundary, layered on the example suite.\n9. [Installation and Distribution](docs/09-installation-and-distribution.md): the consumer and author flows, baking, and the toolchain bootstrap.\n\n## Requirements\n\n- **Java 22 or newer.** clj-zig uses the finalized Foreign Function \u0026 Memory\n  API (JEP 454); `--enable-preview` is not required. The only flag the JVM\n  needs is `--enable-native-access=ALL-UNNAMED`, which the `:test` alias sets.\n- **Zig 0.16, for an author.** clj-zig shells out to `zig` to compile\n  generated source. It uses a `zig` on the path, or fetches a pinned one on\n  first use (see below). A consumer of a library whose native code is\n  already baked needs no Zig.\n- **Clojure CLI** (`deps.edn`, not Leiningen).\n\nDevelopment runs on JDK 26. If your shell's default JDK is older (for example\nthrough sdkman), point the Clojure CLI at JDK 26 for one invocation using\n`JAVA_CMD`:\n\n```bash\nJAVA_CMD=\"$(/usr/libexec/java_home -v 26)/bin/java\" clojure -M:test\n```\n\n## Installation\n\nAdd clj-zig to your `deps.edn` and open native access at runtime. Until a\nrelease is on Clojars, depend on it from git, pinning a commit:\n\n```clojure\n{:deps {io.github.leifericf/clj-zig {:git/sha \"\u003ccommit-sha\u003e\"}}\n :aliases\n {:dev {:jvm-opts [\"--enable-native-access=ALL-UNNAMED\"]}}}\n```\n\nOnce published, depend on the released version from Clojars instead. The git\ncoordinate (`io.github.leifericf/clj-zig`) and the Clojars coordinate\n(`com.leifericf/clj-zig`) differ; releases are dated:\n\n```clojure\n{:deps {com.leifericf/clj-zig {:mvn/version \"2026.06.17-alpha1\"}}\n :aliases\n {:dev {:jvm-opts [\"--enable-native-access=ALL-UNNAMED\"]}}}\n```\n\nThat one JVM flag is the only step native access requires; a running JVM\ncannot grant it to itself, so clj-zig cannot remove it. Without it, clj-zig\nreports `:clj-zig/native-access-disabled` naming the flag.\n\nAn author also needs a `zig` compiler. clj-zig uses one on the path, or\nfetches a pinned Zig into `.clj-zig/zig/\u003cversion\u003e/` on first use and reuses\nit, so installing Zig by hand is optional. On macOS with Homebrew:\n\n```bash\nbrew install zig clojure/tools/clojure\nbrew install --cask temurin\n```\n\nA consumer of a library whose native code is baked needs no Zig at all. The\n[Installation and Distribution](docs/09-installation-and-distribution.md)\nguide covers the consumer flow, the author bake-and-publish flow, and the\ntoolchain bootstrap.\n\n## Running the tests\n\n```bash\nclojure -M:test\n```\n\nPure-core tests (signature, type, spec, source) run on any JDK 22+. The shell\ntests compile and load native code, so they need `zig` on the path and JDK 22+.\n\n## Non-goals for the proof of concept\n\n- No Zig-to-Clojure callbacks.\n- No embedded JVM from Zig.\n- No arbitrary Clojure object marshalling.\n- No hiding of Zig's type system.\n- No production packaging before the REPL experience is proven.\n- No DSL that pretends to be Zig but is not.\n\n## Acknowledgements\n\nclj-zig grew out of several conversations with my friend\n[@teodorlu](https://github.com/teodorlu) in the Norwegian Clojure community.\n\n## License\n\nReleased under the [MIT License](LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fleifericf%2Fclj-zig","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fleifericf%2Fclj-zig","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fleifericf%2Fclj-zig/lists"}