Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

  1. Installing jank on macOS and Linux
  2. Writing a program which prints Hello, world!
  3. 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 new without specifying org.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.

  1. The project name hello_lein and version 0.1-SNAPSHOT
  2. :license: The license of our project, which defaults to MPL since jank uses it. You are free to change this.
  3. :dependencies: Our project dependencies. More on this in another chapter.
  4. :plugins: The lein-jank plugin, and its :middleware, which is used to configure our project for jank instead of Clojure JVM.
  5. :main: The entrypoint of our program, which contains our -main function.
  6. :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:

  1. Embedding raw C++ into your jank programs
  2. Bringing native C and C++ libraries into your jank programs
  3. 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.

  1. Include search paths
  2. Preprocessor defines
  3. Linker search paths
  4. 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 for libcompress.a and libcompress.so (or libcompress.dylib on macOS). Do not put the full file name in :linked-libraries unless 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 FOO or -D FOO=bar to 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:

  1. Numbers (integers and floats)
  2. Bools
  3. 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/raw strings 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::rand becomes cpp/rand
  • std::this_thread::get_id() becomes cpp/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 jank binary, not shipped separately
  • No nested require support (same as ClojureScript)
  • No import
  • (hash-map) returns a hash map, not an array map
  • aget is a special form
  • aset is a macro
  • keyword is more strict about valid inputs

Object model

  • No stable boxes for small integers (the JVM pre-allocates 1, 2, 3, etc)
  • persistent_string is 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)

Compilation model

  • Source-only distribution
    • .o files 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 .class files
    • A module is backed by either a .jank, .cljc or .cpp source file, optionally with a .o file cached for quick loading

Math

  • Division by integer 0 is undefined behavior
  • Division by floating point 0.0 is 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:

  1. How to check the health of your jank install
  2. How to get a stack trace for a jank crash
  3. 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 -v to 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 throw command in gdb before you run.

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 you run.

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.