Welcome to the jank alpha!
by Jeaye Wilkerson, without LLMs, with feedback from the jank community.
This book is written for jank’s alpha release. It is incomplete, although its incompleteness matches jank’s. It’s still early to jump into jank, but your time and patience is welcome.
Important
jank is alpha quality software. It will crash. It will leak. It will be slow. There are huge areas of functionality which haven’t been implemented. Your help getting us past this stage is greatly appreciated.
What is jank?
jank is a general purpose programming language. It’s a dialect of Clojure, which is itself a dialect of Lisp. jank is functional-first, but it supports adhoc mutations and effects. All data structures are persistent and immutable by default and jank, following Clojure’s design, provides mechanisms for safe mutations for easy concurrency.
Beyond Clojure, jank is brethren to C++ and it can reach into C++ arbitrarily to both access and define new C++ types, functions, and templates, at runtime. This is done by JIT (just in time) compiling C++, using Clang and LLVM. The result is that you can write Clojure code which can access C and C++ libraries trivially.
For more details on jank’s status, please read the foreword.
Foreword
jank is a personal creation made public. It’s a passion project which aspires to push the boundaries of two languages very dear to me: C++ and Clojure. These two languages could not be further apart, in their syntax, paradigms, culture, adoption, and typical use cases. Still, I aim to bind them.
jank is currently alpha quality software. Most importantly, that means that you would be crazy to ship it into production for anything that matters. More specifically, it means that jank, and its related programs, will crash, leak memory, provide incorrect results, and in general surprise, confound, and frustrate until we can implement all remaining pieces and iron out all remaining bugs. I need your help with this.
Before jank, achieving this level of seamless C++ interop, with JIT (just in time) compiled C++, and full AOT (ahead of time) compilation support had never been done, from any dynamic language. Swift has come closest, though it’s not dynamically typed and it lacks an official JIT compiler. Cppyy, for Python, strives to compete, but it lacks AOT compilation support. Because of this trail blazing, we have faced many bugs in Clang. Clang is the main challenge for both compile-time performance and overall memory usage. This continues to be a limiter for jank’s success, due to the size and complexity of the Clang code base and my limited time. To ensure jank’s success, this challenge will need to be tackled directly, most likely by finding and employing a part-time Clang developer. If you are able to help with this, please reach out.
Moving on.
The performance of jank, during this alpha stage, will be quite bad. Depending on the benchmark, you might find jank to be 2x or even 10x slower than Clojure JVM. Maybe more.
Do not be concerned by this. I am not concerned by this.
I have not had the luxury to focus on performance much beyond some early design decisions. Clojure JVM, aside from leaning on the JVM for much of its performance, is doing many more optimizations than jank is currently doing. A lot of functionality, such as the GC (BDWGC), sorted containers, and others are currently placeholder. What’s most important is that jank works and that it’s correct. Once we achieve that, I will endeaver to show that jank can be the fastest Clojure dialect around. Right now, that is not a priority, no matter how much you may want it to be.
Lastly, the Clojure community is empowered by backward compatibility. I respect and appreciate this goal for both Clojure and jank. I will be codifying stable APIs for embedding jank, ensuring the stability of jank’s special forms, developing an integrated build system to improve the longevity of jank libraries which wrap native libraries, and pursuing binary compatibility as much as the native world and all of its quirks allows. However, during the alpha release stage, and until we have our first production release, anything goes.
Now, install jank. Build some software. Report all of your bugs on Slack or Github! Engage in the community. Work with us as we stabilize and forge this language into what others in the future will know it to be.
Jeaye
Getting Started
Let’s jump into jank! There’s a lot to learn, but we all have to start somewhere. In this chapter, we’ll discuss:
- Installing jank on macOS and Linux
- Writing a program which prints
Hello, world! - Using Leiningen to manage jank projects
Installation
jank has continuous builds for macOS, Ubuntu, and Arch. These builds are bleeding edge and you’re encouraged to update regularly. If you’re on any of the supported systems, you can install jank using your system’s package manager. If not, you can still build jank yourself.
Homebrew (macOS, aarch64)
We have a binary jank package in brew, so installation is quick and easy.
brew install jank-lang/jank/jank
To update jank, you can run the following.
brew update
brew reinstall jank-lang/jank/jank
If you’d like to install from source using brew, you can use jank-lang/jank/jank-git instead.
Note
We don’t yet have x86 binaries in the Homebrew package. If you’d like to help with this, please reach out.
Ubuntu Linux (24.04, 24.10, 25.04)
We have a binary jank package in our own repo, so installation is quick and easy.
sudo apt install -y curl gnupg
curl -s "https://ppa.jank-lang.org/KEY.gpg" | gpg --dearmor | sudo tee /etc/apt/trusted.gpg.d/jank.gpg >/dev/null
sudo curl -s -o /etc/apt/sources.list.d/jank.list "https://ppa.jank-lang.org/jank.list"
sudo apt update
sudo apt install -y jank
To update jank, you can run the following.
sudo apt update
sudo apt reinstall jank
Note
Older versions of Ubuntu, like 22.04, will not work with jank. This is because jank requires C++20 to work and the libstdc++ on those systems is too old.
Arch Linux (AUR)
We have a binary jank package in AUR, so installation is quick and easy.
yay -S jank-bin
To update jank, you can run the following.
yay -Syy
yay -S jank-bin
If you’d like to install from source on Arch, you can install jank-git instead.
Something else?
Don’t see your preferred system here? Help us with packaging! We want jank to be everywhere.
Hello, world!
Now that you’ve installed jank, it’s time to write your first jank program.
Following tradition, we’ll write a trivial program which prints Hello, world!
to the screen.
Project directory setup
jank doesn’t require any particular directory structure. It can work with files directly. However, in order to keep our systems clean, we’ll create a new project directory for this example. Run these commands in a terminal.
$ mkdir -p ~/projects/hello_world
$ cd ~/projects/hello_world
Next, use your text editor to create a file called hello.jank in the
hello_world directory. The contents should look like this.
(println "Hello, world!")
Finally, we can run this file!
$ jank run hello.jank
Hello, world!
Hello, Leiningen!
jank, on its own, is just a compiler and runtime. For non-trivial projects, you will want a tool to manage your dependencies, profiles, resources, and development workflows. For this, we use Leiningen (LINE-ing-en). Leiningen is a project management tool for Clojure and jank is a dialect of Clojure.
Note
In the future, jank’s recommended workflow will be to use the default Clojure CLI tool, but it’s still being improved and it doesn’t yet offer the excellent user experience that Leiningen does. For now, it is not recommended for jank projects.
Installing Leiningen
Leiningen is available in basically every package manager as leiningen.
# If you're on macOS.
$ brew install leiningen
# If you're on Ubuntu (or similar).
$ sudo apt install -y leiningen
# If you're on Arch (or similar).
$ yay -S leiningen
For more details, see the Leiningen docs.
Creating a project with Leiningen
Now that Leiningen is installed, we can use it to create a new jank project. Let’s create another hello world style program.
$ mkdir ~/projects
$ cd ~/projects
$ lein new org.jank-lang/jank hello_lein
$ cd hello_lein
Note
If you use
lein newwithout specifyingorg.jank-lang/jank, you will get a Clojure JVM project, not a jank project. Make sure you get a jank project.
The layout of a Leiningen project
Inside the hello_lein directory, you will find some files have already been
created.
$ ls
LICENSE project.clj src test
Most importantly, the project.clj is the file which controls Leiningen and
stores all meta information about your project. To start with, our project.clj
will look similar to this:
(defproject hello_lein "0.1-SNAPSHOT"
:license {:name "MPL 2.0"
:url "https://www.mozilla.org/en-US/MPL/2.0/"}
:dependencies []
:plugins [[org.jank-lang/lein-jank "0.2"]]
:middleware [leiningen.jank/middleware]
:main hello-lein.main
:profiles {:debug {:jank {:optimization-level 0}}
:release {:jank {:optimization-level 2}}})
Your versions may differ, but the overall structure will remain. Our
project.clj defines some useful aspects to Leiningen.
- The project name
hello_leinand version0.1-SNAPSHOT :license: The license of our project, which defaults to MPL since jank uses it. You are free to change this.:dependencies: Our project dependencies. More on this in another chapter.:plugins: Thelein-jankplugin, and its:middleware, which is used to configure our project for jank instead of Clojure JVM.:main: The entrypoint of our program, which contains our-mainfunction.:profiles, which allows us to enable different flags and build modes.
Inside src/hello_lein/main.jank, we will see the code for our project.
(ns hello-lein.main)
(defn -main [& args]
(println "Hello, world!"))
Running a Leiningen project
Running your project involves starting at our :main file, loading all required
files, and then calling the -main function. Leiningen will help us with
setting everything up so that jank can do this.
$ lein run
Hello, world!
Testing a Leiningen project
Leiningen has support for easily running all tests for a project. Tests are
written in the test/ directory. The jank template provided us with an example
test which will fail.
$ lein test
Note
This is not yet supported in jank. Accomplishing this requires finding all test namespaces and running them using
clojure.test.
Compiling a Leiningen project
It’s possible to AOT (ahead of time) compile our whole Leiningen project to an executable. This involves compiling every one of our source files and dependencies and then linking them all together. Leiningen makes this easy.
$ lein compile
$ ./a.out
Hello, world!
As with GCC, Clang, etc, the default output name is a.out. When we invoke
that, we see our printed hello world.
Note
There is not yet a way to change the output name using Leiningen, but this will be implemented.
Reaching into C++
jank is designed to be able to reach right into C++ and access variables, types,
functions, templates, and even preprocessor values. All interop is done using
the special cpp/ namespace.
In this chapter, we’ll dive into:
- Embedding raw C++ into your jank programs
- Bringing native C and C++ libraries into your jank programs
- Working with native values
Embedding raw C++
jank has a special cpp/raw form which accepts a single string containing
literal C++ code. This can be used for bringing in pretty much anything. For
example, we can use this to include header files.
(cpp/raw "#include <fstream>")
jank will always compile the included C++ source in a global scope, even if you
put the cpp/raw form within a nested scope, such as within a function or a
let. For example, this code will have the same effect, even if this function
is never called.
(defn foo []
(cpp/raw "#include <fstream>"))
The cpp/raw form always evaluates to nil. At runtime, foo will do nothing
but return nil, since the JIT compilation is where the effect of cpp/raw
actually happens.
A helpful idiom
Hopefully this becomes a less common idiom simply by not being needed, but for
now it’s common enough. If you run into issues trying to access a member, call a
function, etc using normal C++ interop, you can write a wrapper in cpp/raw
which will do the trick. For example, let’s say we have the following code.
(let [s (cpp/std.string)
; Let's say that this interop call isn't compiling correctly, due to a
; jank bug.
size (cpp/.size s)]
(println "The size is" size))
You can work around this issue by defining a helper function which does the C++ work for you. In this case, we could do the following.
(cpp/raw "size_t get_string_size(std::string const &s)
{ return s.size(); }")
(let [s (cpp/std.string)
size (cpp/get_string_size s)]
(println "The size is" size))
Of course, if you need to use this, please also report a bug on jank’s Github which describes what you tried to do and why it didn’t work.
Bringing in native libraries
Ultimately, jank exists to combine Clojure and C++. Most non-trivial jank programs will end up reaching into the C++ world for something. jank embraces familiar concepts from the C++ world, since it’s built entirely around Clang.
- Include search paths
- Preprocessor defines
- Linker search paths
- Libaries to link
Using Leiningen
To bring in a C or C++ library, you will first need to #include the necessary
header files. In order to be able to include them, you need to tell jank (and
thus Clang) where to find them. This is done with your include search paths.
Next, you may need to define some preprocessor macros in order to use or customize the library.
Finally, you may need to link to some libraries involved. This often includes library search paths as well as library names, but library names can also be absolute paths, if that’s applicable to you.
Creating a native lib for testing
Let’s make a new Leiningen project and a small C++ project within it.
$ lein new org.jank-lang/jank native-lib-tutorial
$ cd native-lib-tutorial
$ mkdir native-lib
$ cd native-lib
In this native-lib directory, let’s make a small C++ library which uses libz
to compress the contents of a C++ string. Create a compress.hpp file with
these contents.
#pragma once
#include <string>
namespace native_lib
{
std::string compress(std::string const &input);
}
This is our header file. Now we can create a source file which implements our function.
#include <zlib.h>
#include <stdexcept>
#include "compress.hpp"
namespace native_lib
{
std::string compress(std::string const &input)
{
if(input.empty())
{
return {};
}
auto const input_len{ input.size() };
auto output_size{ compressBound(input_len) };
std::string out;
out.resize(output_size);
auto const res{ ::compress((unsigned char *)out.data(),
&output_size,
(unsigned char *)input.data(),
input_len) };
if(res != Z_OK)
{
throw std::runtime_error{ "compress failed: " + std::to_string(res) };
}
out.resize(output_size);
return out;
}
}
Our source file defines this compress function using zlib. We can now compile
this to a shared library so it can be used in jank.
# Linux.
$ clang++ -shared -o libcompress.so -lz compress.cpp
$ ls
compress.cpp compress.hpp libcompress.so
# macOS.
$ clang++ -shared -o libcompress.dylib -lz compress.cpp
$ ls
compress.cpp compress.hpp libcompress.dylib
Linking to our native lib
Back in our jank project directory, let’s try to use our new library. We’ll start by doing everything incorrectly, so we can see the types of errors jank will raise and how to fix them.
To start with, let’s update our main.jank to include our compress.hpp header
from our native lib.
(ns native-lib-tutorial.main)
(cpp/raw "#include <compress.hpp>")
(defn -main [& args]
(println "Hello, world!"))
When we try to run this project now, jank will fail to compile the code.
$ lein run
In file included from <<< inputs >>>:1:
input_line_1:4:10: fatal error: 'compress.hpp' file not found
4 | #include <compress.hpp>
| ^~~~~~~~~~~~~~
error: Parsing failed.
─ internal/codegen-failure ─────────────────────────────────────────────────────────────────────────
error: Unable to compile C++ source.
This is where include directories come into play. Let’s update our project.clj
to fix this issue!
(defproject native-lib-tutorial "0.1-SNAPSHOT"
:license {:name "MPL 2.0"
:url "https://www.mozilla.org/en-US/MPL/2.0/"}
:dependencies []
:plugins [[org.jank-lang/lein-jank "0.2"]]
:middleware [leiningen.jank/middleware]
:main native-lib-tutorial.main
; Look here!
:jank {:include-dirs ["native-lib"]}
:profiles {:debug {:jank {:optimization-level 0}}
:release {:jank {:optimization-level 2}}})
Now we can run the project again.
$ lein run
Hello, world!
So our jank code is including the native lib header, but we’re not yet doing
anything with it. Let’s call our actual compress function from jank now. Update the -main
function within main.jank to look like the following.
(defn -main [& args]
(if (empty? args)
(println "Try passing some data to compress!")
(let [input (first args)
output (cpp/native_lib.compress input)]
(println "input size" (count input) "output size" (count output)))))
Again, we’re intentionally forgetting a step so we can see what happens. Let’s try to run this now!
$ lein run
Try passing some data to compress!
$ lein run "This is some data to compress!"
JIT session error: Symbols not found: [ _ZN10native_lib8compressERKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE ]
error: Failed to materialize symbols: { (main, { _ZN3jtl6detail5panicINS_6resultIN4jank7runtime4orefINS4_3varEEENS_16immutable_stringEEEEEvRKT_, __orc_init_func.incr_module_3, DW.ref.__gxx_personality_v0, __clang_call_terminate, _ZZSt26__throw_bad_variant_accessjE9__reasons, _ZTSN19native_lib_tutorial4main7_main_1E, _ZNK19native_lib_tutorial4main7_main_115get_arity_flagsEv, _ZTIN19native_lib_tutorial4main7_main_1E, _ZN4jank7runtime8make_boxINS0_3obj17persistent_stringEJRA12_KcEQsr8behaviorE11object_likeIT_EEENS0_4orefIS7_EEDpOT0_, _ZNSt18bad_variant_accessD0Ev, $.incr_module_3.__inits.0, _ZTVSt18bad_variant_access, _ZSt26__throw_bad_variant_accessj, _ZNK3jtl6resultIN4jank7runtime4orefINS2_3varEEENS_16immutable_stringEE10expect_errEv, _ZN19native_lib_tutorial4main7_main_1C2Ev, _ZTSSt18bad_variant_access, _ZTISt18bad_variant_access, _ZN4jank7runtime8make_boxINS0_3obj17persistent_stringEJRA37_KcEQsr8behaviorE11object_likeIT_EEENS0_4orefIS7_EEDpOT0_, _ZN4jank7runtime8make_boxINS0_3obj17persistent_stringEJRA11_KcEQsr8behaviorE11object_likeIT_EEENS0_4orefIS7_EEDpOT0_, _ZN4jank7runtime7convertINSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEE11from_objectENS0_4orefINS0_6objectEEE, _ZNKSt18bad_variant_access4whatEv, _ZTVN19native_lib_tutorial4main7_main_1E, _ZN19native_lib_tutorial4main7_main_14callEN4jank7runtime4orefINS3_6objectEEE, _ZN4jank7runtime3obj12jit_functionD2Ev, _ZN4jank7runtime8make_boxERKN3jtl21immutable_string_viewE, _ZN19native_lib_tutorial4main7_main_1D0Ev, _ZNK4jank7runtime4orefINS0_3obj17persistent_stringEE5eraseEv }) }
─ internal/codegen-failure ─────────────────────────────────────────────────────────────────────────
error: Unable to compile C++ source.
Uh oh! There’s a huge JIT (just in time) linker error. If we focus on the first
line, we can see Symbols not found and then this:
_ZN10native_lib8compressERKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE
This is a C++ mangled symbol, so if you plug it into something like demangler, you can see that it demangles to:
native_lib::compress(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>> const&)
That’s our compress function!
All this means is that the C++ JIT runtime wasn’t able to find a definition for
that symbol. The reason is that we didn’t link our shared library in. Let’s do
that in project.clj now. If we expand on the include directory we added
already, we can specify a library directory and the name of our library.
:jank {:include-dirs ["native-lib"]
:library-dirs ["native-lib"]
:linked-libraries ["compress"]}
With this change, we can now run our whole program.
$ lein run "This is some data to compress! Ideally, the output is smaller than the input."
input size 77 output size 74
$ lein run "Repeated strings are easier to compress. Repeated strings are easier to compress."
input size 81 output size 52
Note
The way native linking works is that you link to
compress, but the linker will actually look forlibcompress.aandlibcompress.so(orlibcompress.dylibon macOS). Do not put the full file name in:linked-librariesunless you’re also specifying the absolute path.
Finally, if we AOT (ahead of time) compile this project down to an executable, we can run it without Leiningen.
$ lein compile
$ ./a.out
./a.out: error while loading shared libraries: libcompress.so: cannot open shared object file: No such file or directory
Oh no! Our libcompress.so isn’t found, when we try to run our compiled
executable. If we inspect the binary with ldd (or otool -L on macOS), we can
see the linked libraries. Note how libcompress.so is not found.
$ ldd a.out
linux-vdso.so.1 (0x00007fa6f0f64000)
libm.so.6 => /usr/lib/libm.so.6 (0x00007fa6ebadd000)
libLLVM.so.22.0git => /home/jeaye/projects/jank/compiler+runtime/build/llvm-install/usr/local/bin/../lib/libLLVM.so.22.0git (0x00007fa6e7000000)
libclang-cpp.so.22.0git => /home/jeaye/projects/jank/compiler+runtime/build/llvm-install/usr/local/bin/../lib/libclang-cpp.so.22.0git (0x00007fa6e2c00000)
libcrypto.so.3 => /usr/lib/libcrypto.so.3 (0x00007fa6e268c000)
libz.so.1 => /usr/lib/libz.so.1 (0x00007fa6ebac4000)
libzstd.so.1 => /usr/lib/libzstd.so.1 (0x00007fa6e25a7000)
libcompress.so => not found
libstdc++.so.6 => /usr/lib/libstdc++.so.6 (0x00007fa6e2313000)
libgcc_s.so.1 => /usr/lib/libgcc_s.so.1 (0x00007fa6eba95000)
libc.so.6 => /usr/lib/libc.so.6 (0x00007fa6e2101000)
/lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2 (0x00007fa6f0f66000)
libedit.so.0 => /usr/lib/libedit.so.0 (0x00007fa6eba59000)
libxml2.so.16 => /usr/lib/libxml2.so.16 (0x00007fa6e1fcc000)
libncursesw.so.6 => /usr/lib/libncursesw.so.6 (0x00007fa6eb9ea000)
libicuuc.so.78 => /usr/lib/libicuuc.so.78 (0x00007fa6e1dbe000)
libicudata.so.78 => /usr/lib/libicudata.so.78 (0x00007fa6dfe29000)
Getting around this varies based on situation, but a quick workaround is to tell the dynamic linker where else to look, when we run our program.
# Linux.
$ LD_LIBRARY_PATH=native-lib ./a.out "ABABABABABABABABABAB"
input size 20 output size 12
# macOS.
$ DYLD_LIBRARY_PATH=native-lib ./a.out "ABABABABABABABABABAB"
input size 20 output size 12
Note
jank does not support C++20 modules right now. In fact, most C++ compilers have very poor support for C++20 modules right now. This may come, in the future, but it will only happen once the experience is sane.
Using jank directly
jank exposes the same flags as Clang for includes, defines, and linked libraries. They work in the same way, too. For all of these, you may add as many as you need.
- Specify
-I <path>to add a new include path - Specify
-D FOOor-D FOO=barto add a new preprocessor define - Specify
-L <path>to add a new library path - Specify
-l <lib>to link to a library name
Working with native values
C++ values, references, and pointers can be used directly within jank. However,
there are some limitations due to how C++’s object model works. The primary
factor here is that C++ has no base object type for all classes and structs.
Each top-level type is standalone. This is different from the JVM, CLR, and JS
environments where there is a base Object for all class types. This means that
we need to take some extra steps in order to store arbitrary C++ values within
the jank runtime. There are a few ways this can be done and jank tries to make
this as easy as possible.
Tagged literals
jank provides access to literal C++ values through the #cpp reader tag. Using
this will resolve to a C++ primitive, rather than a jank runtime object. It’s
supported for the following literals:
- Numbers (integers and floats)
- Bools
- Strings
For example:
(let [i #cpp 0
f #cpp 3.14159
s #cpp "meow"]
)
Named literals
jank also has explicit support for cpp/true, cpp/false, and cpp/nullptr,
which all correspond to the C++ primitive.
Member values
Member values can be accessed from a native object using the cpp/.-foo syntax.
For example, let’s create a person and then pull out the name.
(cpp/raw "struct person
{ std::string name; };")
(defn create-person [name]
(let [p (cpp/person (cpp/cast cpp/std.string name))
n (cpp/.-name p)]
))
Whenever a member is accessed, you will get a reference to it, not a copy. Also, note that members can be access through a pointer to the native object, without needing an explicit dereference.
(defn create-person [name]
(let [p (cpp/new cpp/person (cpp/cast cpp/std.string name))
n (cpp/.-name p)]
))
Trait-convertible
Some C++ types are automatically and implicitly convertible to/from
jank objects. These include all C++ intrinsic intregral types, bools, C strings,
and even some C++ standard libary types like std::string. For these types,
the jank compiler will detect if conversion is necessary and will implicitly
handle conversions as needed. For example, let’s take a look at this jank code
which calls a C++ function which operates on std::string.
(cpp/raw "std::string to_upper(std::string const &s)
{
std::string ret;
for(auto const c : s)
{ ret += ::toupper(c); }
return ret;
}")
(defn to-upper [o]
(let [upper (cpp/to_upper o)]
upper))
In this code, o is a jank::runtime::object_ref. This is basically like
Clojure’s Object type. It’s a garbage collected, type-erased value. When the
jank compiler analyzes the call to (cpp/to_upper o), it resolves that
to_upper expects a std::string and that there is a conversion trait for it.
So the jank compiler will automatically handle converting from object_ref into
a std::string. On the other side, upper is a std::string, which is the
result of to_upper. However, when we return it from the let, the jank
compiler sees that it can implicitly create an object_ref for us, so there’s
nothing we need to do.
Non-trait-convertible
Aside from the built-in supported trait conversions, every other C++ type will not be convertible. If you try to pass such a value as a jank function argument or if you try to return such a value from a jank function, you will get a compiler error. For example, given this source:
(cpp/raw "struct person
{ std::string name; };")
(defn create-person [name]
(let [p (cpp/person (cpp/cast cpp/std.string name))]
p))
If we try to run this file, we’ll get a compiler error telling us that we can’t
return a value of type person from our function, since it’s not convertible to
a jank runtime object.
$ jank run person.jank
─ analyze/invalid-cpp-conversion ────────────────────────────
error: This function is returning a native object of type
'person', which is not convertible to a jank runtime
object.
─────┬───────────────────────────────────────────────────────
│ test.jank
─────┼───────────────────────────────────────────────────────
2 │ { std::string name; };")
3 │
4 │ (defn create-person [name]
│ ^ Expanded from this macro.
─────┴───────────────────────────────────────────────────────
Implementing your own trait
To build on the person type defined above, we could extend the conversion
trait to teach jank how to convert to/from person and jank maps. This does
require C++ template metaprogramming, which is an advanced concept that’s only
intended for C++ developers who’re using jank.
(cpp/raw "struct person
{ std::string name; };
namespace jank::runtime
{
template <>
struct convert<person>
{
static obj::keyword_ref name_kw;
static obj::persistent_hash_map_ref into_object(person const &p)
{
return obj::persistent_hash_map::create_unique(std::make_pair(name_kw, make_box(p.name)));
}
static person from_object(object_ref const o)
{
auto const name{ try_object<obj::persistent_string>(get(o, name_kw)) };
return person{ name->data };
}
};
obj::keyword_ref convert<person>::name_kw{ __rt_ctx->intern_keyword(\"name\").expect_ok() };
}")
(defn create-person [name]
(let [p (cpp/person (cpp/cast cpp/std.string name))]
p))
(println (create-person "foo"))
Now, if we’re to run this, we can see that the person was implicitly converted
into a jank hash map.
$ jank run person.jank
{:name foo}
Note
Although this works, consider moving this C++ into a header file and including it instead. Writing large amounts of C++ in
cpp/rawstrings doesn’t scale very well.
Opaque boxes
There is a performance cost to the convenience of implicit conversions. For pure data, and trivial types, this may be preferred. However, if you want to store something like a C++ database handle, which is managing network resources, a thread pool, and other state, converting this to a jank runtime object is not practical. In these cases, you can use an opaque box to pass the data through the jank runtime instead.
Opaque boxes are jank runtime objects which basically store a void*, which is
an untyped native pointer. The key part here is that the data you store in the
opaque box must be a pointer. Since the opaque box could be stored in a
container, captured in a closure, or otherwise kept alive, it’s important that
the data within is also dynamically allocated. However, opaque boxes track the
name of the type at compile-time and ensure that unboxing uses the correct type.
Given a hypothetical my_db C++ database library, boxing is done like this:
(defn query! [db-box q]
(let [; db-box is an object_ref
; db is a my_db.connection*
db (cpp/unbox cpp/my_db.connection* db-box)]
(cpp/.query db q)))
(defn -main [& args]
(let [; db is a my_db.connection*
db (cpp/new cpp/my_db.connection #cpp "localhost:5758")
; db-box is a opaque_box_ref
db-box (cpp/box db)]
))
If you unbox the incorrect type, jank will surface a runtime error with helpful
source information describing the type that was in the opaque box and the type
you expected. For example, let’s say we box a connection*, but we try to unbox
it as a secure_connection*.
❯ jank run test.jank
─ runtime/invalid-unbox ───────────────────────────────────────────────────────
error: This opaque box holds a 'my_db::connection*', but it was unboxed as a
'my_db::secure_connection*'.
─────┬─────────────────────────────────────────────────────────────────────────
│ test.jank
─────┼─────────────────────────────────────────────────────────────────────────
21 │ (let [; db-box is an object_ref
22 │ ; db is a my_db.connection*
23 │ db (cpp/unbox cpp/my_db.secure_connection* db-box)]
│ ^^^^^^^^^ Unboxed here.
│ …
28 │ db (cpp/new cpp/my_db.connection #cpp "localhost:5758")
29 │ ; db-box is a opaque_box_ref
30 │ db-box (cpp/box db)]
│ ^^^^^^^ Boxed here.
31 │ (query! db-box "meow")))
32 │
33 │ (-main)
│ ^^^^^^^ Used here.
─────┴─────────────────────────────────────────────────────────────────────────
Complex literal values
If your C++ value is not representable using jank’s interop syntax, due to
template arguments or other shenanigans, you can use cpp/value to provide the
complete value using an inline C++ string. For example, here’s how we grab the
npos from a template instantiation:
(let [m (cpp/value "std::basic_string<char>::npos")]
)
No implicit boxing will happen here, unless you use this value in a way which requires it. jank will give you a reference to the value you specified. If you need a copy, you will need to manually do that.
Working with native types
Accessing C++ types
C++ types are available under the cpp/ namespace, if you replace :: with
.. For example, std::string becomes cpp/std.string. This also works for
type aliases.
Complex literal types
If your C++ type is not representable using jank’s interop syntax, due to
template arguments or other shenanigans, you can use cpp/type to provide the
complete type using an inline C++ string. For example, here’s how we both grab
the type and construct it (call the type), to get a value. In this example, we
build a C++ ordered map from std::string to pointers to functions which take
in an int and return an int.
(let [m ((cpp/type "std::map<std::string, int (*)(int)>"))]
)
Defining new types
There isn’t yet a way to define new types using jank’s syntax, but you can
always drop to cpp/raw to either include headers or define some C++ types
inline. Improved support for extending jank’s object model with JIT (just in
time) compiled types will be coming in 2026.
Working with native functions
Global functions
C++ has a huge range of function scenarios and jank tries to capture them all.
The simplest case is global functions, as well as static member functions. This
applies to both C and C++ functions. In order to call these, just take the fully
qualified name of the function and replace :: with .. For example:
std::randbecomescpp/randstd::this_thread::get_id()becomescpp/std.this_thread.get_id
For example, we can use the C functions srand, time, and rand to seed the
pseudo-random number generator with the current time and then get a number.
(defn -main [& args]
(cpp/srand (cpp/time cpp/nullptr))
(println "rand:" (cpp/rand)))
Overload resolution
Once we get out of C land and into C++ territory, the function name alone doesn’t necessarily make it unique. C++ functions can be overloaded with different arities and different parameter types. jank will resolve each function call at compile-time. There is no runtime reflection. If a call can’t be resolved to a known overload, or is ambiguous between multiple overloads, jank will raise a compiler error.
For example, the
std::to_string
function has many different overloads. Here, we specifically create i to be an
int, so overload resolution can happen.
(defn -main [& args]
(let [i #cpp 42
s (cpp/std.to_string i)]
s))
However, if we try to rely on implicit trait conversions, or we pass an unsupported type, we’ll get a compiler error.
(defn -main [& args]
(let [i 42
s (cpp/std.to_string i)]
s))
$ jank run test.jank
─ analyze/invalid-cpp-function-call ───────────────────────────────────────────
error: No normal overload match was found. When considering automatic trait
conversions, this call is ambiguous.
─────┬─────────────────────────────────────────────────────────────────────────
│ test.jank
─────┼─────────────────────────────────────────────────────────────────────────
1 │ (defn -main [& args]
2 │ (let [i 42
│ ^ Expanded from this macro.
3 │ s (cpp/std.to_string i)]
│ ^^^^^^^^^^^^^^^^^ Found here.
─────┴─────────────────────────────────────────────────────────────────────────
We could opt into a specific conversion, and thus a specific overload, by using cpp/cast.
(defn -main [& args]
(let [i 42
s (cpp/std.to_string (cpp/cast cpp/int i))]
s))
Member functions
Member functions can be accessed using the cpp/.foo syntax. For example, let’s
convert a jank object to a std::string and then see if it’s empty.
(defn empty? [o]
(let [s (str o)
native-s (cpp/cast cpp/std.string s)]
(cpp/.empty native-s)))
Also note that member functions can be called through a pointer to the native object, without the need for an explicit dereference.
Arbitrary callables
In C++, we also deal with pointers to functions and custom types which implement the call operator. jank supports both of these scenarios using the normal call syntax. For example, we can implement our own callable which captures some data and then returns it when called.
(cpp/raw "struct call_me
{
jank::runtime::object_ref data;
jank::runtime::object_ref operator()()
{ return data; }
};")
(defn -main [& args]
(let [f (cpp/call_me. "meow")]
(f)))
Operators
C++ operators are special language features for primitives, but they can also be
overloaded for custom types. Their semantics are much more complicated than
Clojure’s function calls, but basically all of them are available under the
cpp/ namespace within jank.
Note
C++20 does operator rewriting for comparison operators, to use the
<=>spaceship operator, or perhaps others. jank doesn’t currently support this. If you’re porting C++ code to jank which fails to find the appropriate operator, chances are that operator never existed and Clang used rewriting to use a different operator instead.
The reasoning for the cpp namespace
All forms related to C++ interop in jank are under the special cpp/ namespace.
This is a departure from normal Clojure interop. It was done for simplicity, to
provide some insulation between Clojure and C++.
With that said, it is not yet determined if this namespace will stay. For some
special forms, like cpp/raw, I think it makes sense. Although, for typical day
to day object creation and member access, the cpp/ namespace is likely overly
noisy.
There are concerns with functions like clojure.core/int being ambiguous with
cpp/int. It could be that only types are pulled from cpp/, but consistency
is also sanity.
However, it’s worth considering that cpp/ is useful if jank provides interop
with other native languages, such as Rust. In which case, we may want to
disambiguate with cpp/ and rs/.
As you explore the jank alpha release, please consider this and provide feedback.
Differences from Clojure
jank is meant to be Clojure, but Clojure itself has no specification. There are differences between Clojure JVM, ClojureScript, Clojure CLR, ClojureDart, and others. Part of being a Clojure is embracing one’s host and being transparent about it. This is where most of the differences come into play.
jank does not try to hide its C++ host. That would defeat the point of being Clojure.
clojure.core
- Baked into the
jankbinary, not shipped separately - No nested
requiresupport (same as ClojureScript) - No
import (hash-map)returns a hash map, not an array mapagetis a special formasetis a macrokeywordis more strict about valid inputs
Object model
- No stable boxes for small integers (the JVM pre-allocates
1,2,3, etc) persistent_stringis expected to be UTF-8- No inheritance (currently)
- No records (yet)
- No protocols (yet)
- Sequences
- Support for in-place operations (
fresh-seq,next-in-place)
- Support for in-place operations (
Compilation model
- Source-only distribution
.ofiles found in JARs will not be used- Git deps are an exception here; if someone commits a .o file into a git dep on your module path, jank will load it
- jank uses the term “module path” instead of “class path”
- We don’t have
.classfiles - A module is backed by either a
.jank,.cljcor.cppsource file, optionally with a.ofile cached for quick loading
- We don’t have
Math
- Division by integer
0is undefined behavior - Division by floating point
0.0is well defined
For C++ Developers
jank is not yet ready for C++ developers. I have not stabilized the API for embedding, there are GC concerns within existing C++ applications, the header feeding mechanisms within Clang need additional support for multiple PCHs (pre-compiled headers), and there is no documentation. Don’t think for a minute that this list is exhaustive.
With that said, some have already embedded jank into existing C++ applications. If you’re brave, and impatient, it can work. Otherwise, wait for me to stabilize the Clojure side of things first. The timeline for this is more like the end of 2026.
Troubleshooting
jank is not yet stable. Chances are, you’re going to hit some crashes, bugs, leaks, or other issues. In this chapter, we’ll learn the following:
- How to check the health of your jank install
- How to get a stack trace for a jank crash
- Where to ask for help and report issues
Checking jank’s health
Once jank is installed, you can query its health at any time. Here’s an example output of jank installed via Homebrew on macOS.
$ jank check-health
─ system ───────────────────────────────────────────────────────────────────────────────────────────
─ ✅ operating system: macos
─ ✅ default triple: arm64-apple-darwin25.0.0
─ jank install ─────────────────────────────────────────────────────────────────────────────────────
─ ✅ jank version: jank-0.1-768f8310ce0f3d61b01f2df0f0e66ab9c9df1984
─ ✅ jank resource dir: ../lib/jank/0.1
─ ✅ jank resolved resource dir: /opt/homebrew/Cellar/jank/0.1/bin/../lib/jank/0.1 (found)
─ ✅ jank user cache dir: /Users/jeaye/.cache/jank/arm64-apple-darwin25.0.0-f33ec85999b436c281e9fba631425b57189670f96ba3166f2d327cd1543b516d (found)
─ clang install ────────────────────────────────────────────────────────────────────────────────────
─ ⚠️ configured clang path: /Users/runner/work/jank/jank/compiler+runtime/build/llvm-install/usr/local/bin/clang++ (not found)
─ ✅ runtime clang path: /opt/homebrew/Cellar/jank/0.1/bin/../lib/jank/0.1/bin/clang++ (found)
─ ⚠️ configured clang resource dir: /Users/runner/work/jank/jank/compiler+runtime/build/llvm-install/usr/local/lib/clang/22 (not found)
─ ✅ runtime clang resource dir: /opt/homebrew/Cellar/jank/0.1/lib/jank/0.1/lib/clang/22 (found)
─ jank runtime ─────────────────────────────────────────────────────────────────────────────────────
─ ✅ jank runtime initialized
─ ✅ jank pch path: /Users/jeaye/.cache/jank/arm64-apple-darwin25.0.0-f33ec85999b436c281e9fba631425b57189670f96ba3166f2d327cd1543b516d (found)
─ ✅ jank can jit compile c++
─ ✅ jank can aot compile working binaries
─ support ──────────────────────────────────────────────────────────────────────────────────────────
If you're having issues with jank, please either ask the jank community on the Clojurians Slack or report the issue on Github.
─ Slack: https://clojurians.slack.com/archives/C03SRH97FDK
─ Github: https://github.com/jank-lang/jank
How to read the output
jank’s health check will provide essential information about the jank installation, Clang installation, and current system. In general, if you don’t see any ❌ then you’re good to go. However, for more subtle issues, you may need to look at the particular paths which jank has determined to ensure they match up with your expectations.
Either way, when you’re reporting a bug or submitting system information, including your health check output is very useful.
How to get a stack trace
If jank is crashing, or your AOT compiled program is, you may be asked to provide a stack trace. In case you’re not familiar with how to do this, here’s a quick rundown for both Linux (gdb) and macOS (lldb).
Note
Getting a stack trace requires invoking a debugger with your jank command. If you’re using Leiningen to invoke jank, you can get the underlying command by passing
-vto Leiningen. For example,lein run -v.
Let’s say we’re trying to run jank run foo.jank.
Linux
Make sure you have gdb installed. This is likely already installed, but if
it’s not, it is definitely in your package manager’s repos and it’s likely just
called gdb. Once you have gdb, you can use the following.
$ gdb --args jank run foo.jank
> run
# Do whatever is needed to cause the crash.
> backtrace
# Copy this to share with others.
> quit
Note
If you want to break when an exception is thrown, use the
catch throwcommand in gdb before yourun.
macOS
On macOS, you should have lldb installed as part of your developer tools.
However, you can get newer versions from Homebrew as part of the llvm package.
$ lldb -- jank run foo.jank
> run
# Do whatever is needed to cause the crash.
> backtrace
# Copy this to share with others.
> quit
Note
If you want to break when an exception is thrown, use the
breakpoint set -E c++command in lldb before yourun.
Where to get help
The jank community, which is the Clojure community, is known to be welcoming. You are encouraged to reach out if you have any issues. Firstly, drop into Slack and ask your questions or explain your problems there. This will often be all that you need. However, if you’d like to report an issue for us to work on, please do so on Github.
In either case, please explain to the best of your ability and try to reproduce any issues with the smallest amount of code possible. Most of the time, a jank bug can be reproduced in fewer than 5 lines of code, but it can take some effort to get there.