Inkwell: a typed Rust wrapper over llvm-sys for people writing compilers and JITs
It's a New Kind of Wrapper for Exposing LLVM (Safely)
At a glance
- What is it?
- Inkwell wraps LLVM's C API in Rust types so IR mistakes surface at compile time. Here is what it does, how to wire up an LLVM version feature, and where it stops helping.
- Who is it for?
- Reach for Inkwell when you are writing a compiler, an interpreter backend, or a JIT in Rust and you want LLVM's C API behind Rust types, and when you can pin one LLVM version in your Cargo.toml. Do not adopt it if you need several LLVM versions in one build graph, or if you want to keep calling the C API directly.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 6 days ago.
- What is it written in?
- Mainly Rust, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 25, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What Inkwell is for, and who actually needs it
LLVM's C API is untyped at the boundary. A value is a pointer, a function is a pointer, and nothing stops you from passing the wrong one. The README frames Inkwell as a wrapper that "provides a more strongly typed interface than the underlying LLVM C API so that certain types of errors can be caught at compile time instead of at LLVM's runtime." That sentence is the whole pitch, and it is a narrow one.
The audience is people writing a compiler or a JIT in Rust. The repository ships examples/jit.rs and examples/kaleidoscope/, and the README points at LLVM's Kaleidoscope tutorial as a worked example. If your work is emitting IR, running optimization passes, and executing the result, this is aimed at you. If you only need to read an existing .ll file or call into a prebuilt library, the crate is more machinery than the job requires.
The crate name is an acronym: "It's a New Kind of Wrapper for Exposing LLVM (Safely)." The stated goal is to replicate LLVM IR's strong typing as closely as possible, and to make LLVM easier to learn through documentation.
How the typed layer maps onto LLVM's C API
Inkwell sits on top of llvm-sys, the raw binding crate. Each LLVM version gets its own llvm-sys crate and its own Cargo feature: llvm18-1 pulls in llvm-sys-181, llvm23-1 pulls in llvm-sys-231, and so on through the range the README lists as LLVM 12-23. The feature is not optional. Cargo.toml defines no default LLVM version, so a build without a version feature has nothing to link against.
The object graph follows LLVM's own: a Context owns types and modules, a Module owns functions, a Builder inserts instructions into the current basic block. Lifetimes carry the ownership. In the README's JIT example, CodeGen<'ctx> holds a &'ctx Context alongside a Module<'ctx>, a Builder<'ctx> and an ExecutionEngine<'ctx>, so the borrow checker enforces that nothing outlives the context that created it. That is the mechanism behind the safety claim, and it is the part you cannot get from llvm-sys directly.
The typing shows up in the accessors. function.get_nth_param(0)? returns an Option, and .into_int_value() converts it to an IntValue rather than leaving you with an opaque pointer. build_int_add then takes IntValues and returns a Result. Where LLVM's C API would let a mismatch through to a runtime assertion, the Rust signatures reject it earlier.
Two escape hatches remain, and the README is explicit about why. Getting a JIT-compiled function back out of the execution engine is unsafe, because nothing verifies that the signature you ask for matches the function you compiled. Calling that function is unsafe for the same reason calling into C is: the compiled code may do things Rust cannot check.
Installing Inkwell and JIT-compiling your first function
You need Rust 1.85 or newer and one of LLVM 12 through 23 present on the machine, since llvm-sys links against the installed LLVM libraries. The README gives the dependency line directly, with the version feature selected to match your LLVM install:
[dependencies]
inkwell = { version = "0.10.0", features = ["llvm23-1"] }The naming rule is llvmM-N, where M and N are the LLVM major and minor version. LLVM 18.1.x becomes llvm18-1. If another dependency already installs LLVM and you do not want a second link, Cargo.toml defines a parallel set of features such as llvm18-1-no-llvm-linking, which forward to llvm-sys's own no-llvm-linking flag. The comment in Cargo.toml explains why there is one such feature per version rather than a single shared flag.
The shortest path to something running is examples/jit.rs in the repository. It builds a three-argument sum function, JITs it, and calls it. The core of it looks like this:
let context = Context::create();
let module = context.create_module("sum");
let execution_engine = module.create_jit_execution_engine(OptimizationLevel::None)?;
let i64_type = context.i64_type();
let fn_type = i64_type.fn_type(&[i64_type.into(), i64_type.into(), i64_type.into()], false);
let function = module.add_function("sum", fn_type, None);After positioning the builder at a basic block and emitting two integer adds and a return, the function is retrieved and called:
let sum = codegen.jit_compile_sum().ok_or("Unable to JIT compile `sum`")?;
unsafe {
println!("{} + {} + {} = {}", x, y, z, sum.call(x, y, z));
}What you should see is the printed sum, and the README's version asserts that the JIT result equals x + y + z. If the build fails at the link step instead, the mismatch is almost always between the installed LLVM and the feature you selected.
The LLVM version feature is the main constraint
One build, one LLVM version. That is the direct consequence of the feature design, and it is the limitation most likely to bite in a larger workspace. If two crates in your dependency graph pick llvm18-1 and llvm19-1, Cargo will try to build both llvm-sys crates, and the no-llvm-linking comment in Cargo.toml notes that a single cross-version flag was not possible because it would cause Cargo to download and compile every version. There is no supported way to have Inkwell target two LLVM releases in one binary.
Second, the crate is pre-1.0.0 and says so plainly: "we may make breaking changes on master from time to time since we are pre-v1.0.0, in compliance with semver. Please prefer a crates.io release whenever possible!" Tracking master is a deliberate choice with a known cost. The release history is consistent with that: 0.8.0 in January 2026, 0.9.0 in April, 0.10.0 in August.
Third, the documentation is incomplete by the project's own admission. The deployed docs "are not yet 100% complete and only show the latest supported LLVM version due to a rustdoc issue," with issue #2 cited for detail. If you are targeting an older LLVM, the hosted docs may not describe your version's surface.
Finally, the typed layer covers the common path, not everything. The README's example still needs unsafe for retrieving and calling the JITed function, and the footnote explains both uses. Inkwell reduces the unsafe surface; it does not remove it.
Inkwell versus llvm-ir: two different jobs
The README names one alternative, llvm-ir. The difference is in what each crate treats as the primary object. Inkwell is a builder: you create a context, add functions, position a builder, and emit instructions, with a JIT execution engine available for running the result. llvm-ir works from the other direction, treating LLVM IR as a data structure to parse, inspect, and write back out.
So the choice is about your entry point. If your program generates IR instruction by instruction and may execute it, Inkwell's builder and execution engine match that flow. If your program reads .ll or .bc files, walks the module structure, or transforms existing IR, a representation-first crate fits better. They are not interchangeable, and nothing in the README suggests using both together.
The other alternative, of course, is llvm-sys on its own. That removes the typed layer and the lifetimes, and with them the compile-time checking the crate exists to provide. It is a reasonable choice if you already know the C API well and want the thinnest possible binding.
Upgrade cost and the Apache-2.0 licence
Upgrading Inkwell means moving the LLVM feature, not just the crate version. Bumping from llvm18-1 to llvm19-1 changes which llvm-sys crate is pulled in, so the LLVM installation on your build machines and CI images has to move with it. The release cadence suggests this happens a few times a year, and the pre-1.0.0 status means a version bump can carry breaking API changes on top of the LLVM move. Budget for both at once rather than treating the LLVM feature as a one-line edit.
On licensing, Cargo.toml declares Apache-2.0, and the repository carries a LICENSE file at the top level. That is permissive and generally straightforward for commercial use. What it does not settle is LLVM's own licence, which is separate from Inkwell's and governs the libraries llvm-sys links against. If you are distributing a binary that links LLVM, check LLVM's terms for your version rather than assuming Inkwell's licence covers the whole artifact. This is not legal advice; it is a pointer to the two licences in play.
Editorial conclusion
Reach for Inkwell when you are writing a compiler, an interpreter backend, or a JIT in Rust and you want LLVM's C API behind Rust types, and when you can pin one LLVM version in your Cargo.toml. Do not adopt it if you need several LLVM versions in one build graph, or if you want to keep calling the C API directly. Before committing, install the LLVM version you intend to target, build examples/jit.rs against it, and check the deployed docs for the API you plan to use, since the project states they are not yet 100% complete.
Frequently asked questions
What is Inkwell used for?
It wraps llvm-sys so you can build LLVM IR from Rust with a more strongly typed API, catching certain errors at compile time instead of at LLVM's runtime. The README describes the goal as helping you write your own programming languages.
How do you use Inkwell in a Rust project?
Add it to Cargo.toml with exactly one LLVM version feature, for example inkwell with the llvm23-1 feature, then build a Context, Module and Builder. The repository's examples/jit.rs shows a full JIT-compiled function.
Which LLVM versions does Inkwell support?
The README lists LLVM 12 through 23, each mapped to a Cargo feature named llvmM-N, such as llvm18-1 for LLVM 18.1.x. Rust 1.85 or newer is required.
Does Inkwell remove the need for unsafe code entirely?
No. The README's JIT example still uses unsafe twice, once to retrieve the compiled function from the execution engine and once to call it, because neither the signature nor the compiled code can be verified.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/thedan64-inkwell)