Open-source project
xberg-io/alef avatar
xberg-io/alef

Alef turns one alef.toml into eighteen binding targets, and its own example lags

Generate fully-typed, lint-clean language bindings for Rust libraries across 16 languages

101 stars16 forksRustMIT

At a glance

What is it?
A polyglot binding generator for Rust crates that emits host-language types, stubs, docs, tests and release metadata. Two details decide whether it works for you: the quick start config pins alef_version 0.79.2 while the tool is at 0.103.5, and the file ownership rules are explicit about what generation will overwrite.
Who is it for?
Alef fits a Rust project that ships to several language ecosystems from one crate and wants the bindings, the type stubs, the tests and the release metadata generated rather than hand-maintained, and that accepts a codegen tool with opinions. It does not fit a project with one consumer language, or anyone who wants hand-written bindings with a build step instead.
Can I use it commercially?
Yes. MIT 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 1 day 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 November 13, 2026, and from our analysis. They are not legal advice.

From daily snapshots

Star history

  • xberg-io/alef101 ★ · +7
Star history
Repository2026-08-272026-11-13
xberg-io/alef94101

7 Stars in the last 78 days, measured from the daily GitHub snapshot. Compare with other projects

Editorial analysis

The quick start config names version 0.79.2 and the tool is at 0.103.5

Start with the inconsistency, because it sits in the one field that decides behaviour.

The example configuration on the page is:

toml
[workspace]
languages = ["python", "node", "ffi", "go"]
alef_version = "0.79.2"

[[crates]]
name = "sample_core"
sources = ["src/lib.rs"]
version_from = "Cargo.toml"

`alef_version` is set to 0.79.2. The package version in `Cargo.toml` is 0.103.5, and the three most recent tags are v0.103.5, v0.103.3 and v0.103.2, all published within about a day of each other at the end of September 2026.

So the documented example names a version twenty-four minor releases behind the one you would install, and it does so in a field whose entire purpose is to pin the generator version for a workspace. Anyone copying the quick start verbatim gets a config pointing at a much older Alef, and the mismatch will not announce itself: the file is valid, the schema is the same shape, and generation proceeds under whatever rules that older version applied.

That is the first thing to change in a copied config, and it is worth reading the changelog between the two versions before deciding that nothing else in the example has drifted.

Four install routes with four different costs

Alef requires Rust 1.89 or newer, and the page gives four ways to get it.

From source with Cargo:

bash
cargo install alef --locked

Through cargo-binstall, which is the route worth knowing about:

bash
cargo binstall alef

The page is precise about what that buys and what it costs. cargo-binstall avoids compiling Alef when a matching published artifact is available. It still requires Cargo, and it falls back to `cargo install` from source when no compatible artifact exists, so it is an optimisation rather than a separate toolchain.

Homebrew:

bash
brew install xberg-io/tap/alef

That one installs a bottle when one is available and otherwise may build the formula from source, which means the install time on a given machine is not knowable from the page.

And Scoop on Windows:

powershell
scoop bucket add xberg https://github.com/xberg-io/scoop-bucket
scoop install alef

Scoop installs the published Windows binary.

So the real ranking is binstall first on any platform where an artifact exists, Scoop second because it takes the published binary, and the other two as the always-works fallbacks that can end in a full compile.

One config file, and three different classes of file ownership

The configuration model is small and the ownership rules underneath it are the part that matters, because they answer the question every code generator has to answer.

`[workspace]` holds the shared target languages, tool preferences and pipeline defaults. Each `[[crates]]` entry describes one Rust API surface that becomes one or more published packages. `[crates.<language>]` sections customise module names, package names, feature flags, output paths, field naming, dependency extras and language-specific generation behaviour. And `[[crates.adapters]]`, trait bridge config, service API config and e2e config opt into higher-level generated wrappers where a target supports them.

Now the ownership rules, which are stated plainly rather than left to be discovered by losing work. Generated binding files carry Alef hashes and are overwritten by generation commands. Scaffolded package files are generated once, unless the command explicitly opts into overwrite behaviour. And generated README and API doc files are owned by `alef readme` and `alef docs` respectively.

Three classes, three different answers. Binding files are disposable and you should not edit them. Scaffolded package files are yours after the first write. Documentation files have a named owner command. The staleness checking feature exists for the first class: Alef caches inputs, embeds generation hashes, and verifies whether generated files are up to date.

The command order is the mechanism: generate, scaffold, readme, docs, verify

Once a config exists, the quick start is five commands:

bash
alef generate --format
alef scaffold
alef readme
alef docs --output docs/reference
alef verify

Each one owns a different set of files, which is why they are separate commands rather than one. `generate` writes the bindings and takes `--format` so the output is already in the host language's own style. `scaffold` writes the package files that make the bindings installable, once. `readme` owns the generated README, `docs` writes API documentation to the directory you name, and `verify` is the staleness check.

For a project starting from nothing there is `alef init --lang python,node,ffi`, which writes the initial config and the first generated files. And for the full local pass there is `alef all --format`, which is the set above in one invocation.

Two flags narrow any of it. `--lang python,node` restricts a command to selected targets, and `--crate <name>` restricts it to one configured crate. In a workspace with many crates and many languages those two flags are what make iteration fast, since a full generation across every enabled target is the expensive case rather than the common one.

Eighteen targets in the table, and the summary line says sixteen

The targets table has eighteen rows, and the canonical slug list matches it with eighteen names: `python`, `node`, `wasm`, `ruby`, `php`, `elixir`, `r`, `go`, `java`, `csharp`, `kotlin`, `kotlin_android`, `swift`, `dart`, `gleam`, `zig`, `ffi` and `jni`. The repository description says sixteen languages. So the counts do not agree, and the difference is mostly that two of the eighteen are interop layers rather than languages.

The more useful thing is how the targets differ, because it tells you what you inherit. Python gets PyO3 bindings with type stubs. Node gets a NAPI-RS native addon with `.d.ts` output. WebAssembly gets a wasm-bindgen package. Ruby uses Magnus, Elixir and Gleam both use Rustler, R uses extendr, Dart uses flutter_rust_bridge.

Then there are the languages that go through C. Go gets a cgo package over the generated C FFI layer, Zig a package over the generated C ABI, and Swift a package with Rust bridge support. Java gets a JVM package over the generated native library, Kotlin a JVM package over the generated bindings, Kotlin Android an Android package with generated JNI shims, and C# a .NET package using P/Invoke.

The last row is a `jni` target described as a Rust JNI shim crate exercised by both the `kotlin_android` Android AAR and host-JVM tests, which is a detail worth noting: the Android path is verified by the same tests as the desktop JVM path rather than by a separate harness.

Three ways to extend, and the dynamic one is behind a cargo feature

The stated position is that Alef is opinionated about codegen and neutral about domain, and the `Extension` trait is where that neutrality lives. There are three mechanisms and they differ in what you have to ship.

A linked extension means your consumer crate implements `alef::Extension` and ships a thin CLI binary:

rust
fn main() {
    alef::run_with_extensions(vec![Box::new(MyDomainExtension)])
}

That gives full type safety and is what the page recommends for frameworks that generate an HTTP service API. It also means your users run a Rust binary.

A dynamic extension avoids that. You load a compiled `.so`, `.dylib` or `.dll` that declares a C-ABI factory function, and it works when you cannot ship a Rust binary. The declaration lives in the config:

toml
[[extensions.dylib]]
path = "target/release/libmy_extension.dylib"

The cost is that the factory signature crosses a C ABI rather than staying inside Rust's type system.

The third needs no Rust at all: `[[extensions.template]]` blocks in `alef.toml` pointing at Jinja templates, which the built-in `TemplateExtension` emits.

One detail in the manifest ties these together. `libloading` is an optional dependency behind a feature called `dylib-loader`, so the dynamic path is not available in a default build. If your extension plan depends on it, that feature has to be on, and that is not stated in the extension section of the page.

One dependency uses = because a NuGet package ID depends on its version

Most of the dependency list is unremarkable for a generator. `clap` for the command line, `serde` and `serde_json` for configuration, `schemars` and `jsonschema` for the schema files, `rayon` for parallel generation, `heck` and `convert_case` for turning Rust identifiers into host-language conventions, `blake3` and `sha2` for hashing, `minijinja` for the template engine behind template-only extensions, and `proc-macro2` with `quote` for procedural macros.

One entry is pinned with an equals sign, and the comment above it explains why in more detail than any other dependency:

`icu_casemap` is set to `=2.3.0`, exactly, not with a caret, because the coordinates code publishes the pinned Unicode version these two tables carry as part of the NuGet package-ID contract, and a caret bump would move that version silently. The comment points at `tests/unicode_dependency_pins.rs`, so the pin has a test behind it.

That is a good example of why generator dependencies deserve more care than application ones. The generated .NET package identifier is part of what consumers depend on, so the Unicode version is not an internal detail; it is published surface. A floating dependency would have turned a dependency bump into a silent change to the identity of published packages.

There is also `[package.metadata.cargo-machete]` with a single ignored entry for `tracing`, which is the usual way of recording that a crate appears in a macro expansion rather than a written import.

The repository configures itself with alef.toml, and ships hooks for you to use

The tree is where the project's own dogfooding shows up. Alongside the usual `Cargo.toml`, `CHANGELOG.md`, `CONTRIBUTING.md` and `SECURITY.md`, there are four Alef-related config files at the root: `alef.toml`, `poly.toml`, `poly-base.toml` and `poly-hooks.lock`. The tool that generates bindings is configured by its own configuration format, which is the most direct evidence available that the format works on a real multi-crate workspace.

`.pre-commit-hooks.yaml` is a file most repositories do not have, because it declares hooks for other people's repositories to consume. Whatever pre-commit integration the tool offers is packaged for others rather than only used here.

Release plumbing is explicit too. `.goreleaser.yaml` produces the binaries, and the `binstall` metadata in `Cargo.toml` declares the download URL template, a `tgz` package format, and a `zip` override for `x86_64-pc-windows-msvc`, which is the pairing that makes the four install routes on the page work. `deny.toml` is cargo-deny configuration for licences and advisories, and `renovate.json` handles dependency updates.

The rest is a normal Rust workspace with extras: `rust-toolchain.toml` alongside a declared `rust-version` of 1.89, edition 2024, `benches/`, `schemas/`, `hooks/`, `scripts/`, `skills/`, `Taskfile.yml`, and the agent-facing files `.ai-rulez/` and `.mcp.json`. Licence is MIT and the homepage in the manifest points at the GitHub URL.

Editorial conclusion

Alef fits a Rust project that ships to several language ecosystems from one crate and wants the bindings, the type stubs, the tests and the release metadata generated rather than hand-maintained, and that accepts a codegen tool with opinions. It does not fit a project with one consumer language, or anyone who wants hand-written bindings with a build step instead. Verify four things before you commit: which `alef_version` your config should pin, since the documented example names 0.79.2 and the current release is 0.103.5; which of the three file-ownership classes your files fall in, because binding files carry Alef hashes and are overwritten while scaffolded package files are written once; whether you need the `dylib-loader` feature for dynamic extensions; and whether the tool that consumes its own config, `alef.toml` at the repository root, tells you anything about your own layout. Licence is MIT, and the newest release v0.103.5 shipped on 2026-10-01.

Frequently asked questions

How do I install Alef?

Four routes. cargo install alef --locked builds from source, cargo binstall alef skips compiling when a published artifact matches and falls back to cargo install otherwise, brew install xberg-io/tap/alef uses a bottle when one exists, and Scoop installs the published Windows binary after scoop bucket add xberg. Rust 1.89 or newer is required.

How many languages does Alef generate bindings for?

The targets table has eighteen rows and the canonical slug list has eighteen names, from python and node through to gleam and zig, plus the ffi and jni interop layers. The repository description says sixteen languages, so the counts do not agree.

Will Alef overwrite files I have edited?

It depends on the class of file. Generated binding files carry Alef hashes and are overwritten by generation commands. Scaffolded package files are generated once unless a command explicitly opts into overwrite behaviour, and generated README and API doc files are owned by alef readme and alef docs.

What does alef verify check?

Staleness. Alef caches inputs, embeds generation hashes in generated files, and verifies whether the generated output is up to date. It runs as the last step of the quick start, after generate, scaffold, readme and docs.

How do I add my own codegen logic to Alef?

Three ways: implement alef::Extension in a consumer crate and ship a thin binary for full type safety, load a compiled .so, .dylib or .dll declaring a C-ABI factory function, or declare [[extensions.template]] blocks pointing at Jinja templates with no Rust at all. The dynamic route needs the dylib-loader feature.

Official sources

  1. Official README
  2. Project repository
  3. Release notes
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.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/xberg-io-alef.svg)](https://hysenlabs.com/projects/xberg-io-alef)

Star history chart for your README

markdown
[![Star history](https://hysenlabs.com/chart/xberg-io-alef.svg)](https://hysenlabs.com/projects/xberg-io-alef)

Was this useful?