# ash: Vulkan bindings where the pointer chain is type-checked and nothing is validated

> A Rust wrapper around Vulkan, generated from the registry XML, with no validation layer and everything marked unsafe, whose real contribution is making the create-info pointer chain a compile-time error when you push a struct the registry says cannot extend it.

**ash-rs/ash** — Vulkan bindings for Rust

- Repository: https://github.com/ash-rs/ash
- Stars: 2,353 · Forks: 236
- Language: Rust
- License: Apache-2.0
- Published: 2026-09-30 · Updated: 2026-09-30 · Language: en
- Canonical page: https://hysenlabs.com/projects/ash-rs-ash

## No validation, everything unsafe, and that is the design

The feature list contains one item that should be read before any other, and it is a refusal rather than a capability. There is no validation, and everything is unsafe. Read that in the context of what this project is. A Rust developer picking up a graphics crate reasonably expects some layer of checking, and this one declines to provide it, which means the guarantees you get are the guarantees of the underlying C API plus the ones the project adds deliberately. The one guarantee it does add is specific and worth knowing: lifetime-safety on structs created with the builder pattern. So the objects you assemble with a builder are tracked by the compiler for how long they live, while every call that touches the driver is marked unsafe and is your responsibility. The project is explicit that it aims for the true API without compromises and for convenience features that do not limit functionality, and those two goals are in tension with safety, and the project resolves the tension in favour of fidelity. The practical consequence is that ash saves you from a particular category of mistake, the one where the registry says your struct is wrong, and it does not save you from the category where you have freed a device handle and then used it. Treat it as a typed interface to an unsafe C library rather than as a safe wrapper, and your expectations will match.

## The pointer chain is where the compiler actually helps you

Of the type-safety features, one is a genuine improvement on the C API rather than a convenience, and it is the pointer chain. In C, a create-info struct ends in a void pointer that points at the next struct in a chain of extensions, and nothing checks whether the struct you attached is allowed to extend the one you attached it to. Ash generates the rules. The method for inserting a struct at the front of the chain only accepts a struct that the registry lists as able to extend the receiver, because the generated code maps the registry's own extension-interaction declarations onto marker traits, and only structs appearing in that list implement the method at all. The example is two lines of builder calls pushing two feature structs onto a device create-info, and each push is checked at compile time: 

```rust
let mut device_create_info = vk::DeviceCreateInfo::default()
    .push(&mut corner)
    .push(&mut variable_pointers);
```

So if you try to chain a struct onto a create-info that does not accept it, the code does not compile, rather than producing a structure the driver will reject or misinterpret. There is a second, unsafe method for the case where the struct you are pushing already has a valid chain of its own, and the documentation says so explicitly, which is the right way to handle it: the fast path is checked, the path that needs it is marked unsafe, and the readme tells you which is which. Everything else in the type system follows the same philosophy, which is to be helpful where the registry has something to say and to get out of the way where it does not.

## Function pointers load eagerly, and a missing one panics when you call it

The loader is where you can see the crate's priorities, because the default behaviour is the one that avoids a branch. Function pointers are split into three categories with documented lifetimes. The entry loader loads the Vulkan library and has to outlive the instance and the device. The instance loader loads instance-level functions and has to outlive the devices it created. The device loader loads device-local functions, and those are fetched per device. Everything is loaded by default, and the crucial detail is what happens to the ones that fail: a function that could not be loaded is initialised to a stub that always panics. So there is no optional call in the hot path, and the cost of a missing function is paid at the call site rather than at load time. The readme states the consequence of that choice in one sentence, and it is the sentence a reader should memorise: do not call 1.1 functions if you have created a 1.0 instance, because doing so results in a panic. That is a real constraint on a project that supports versions 1.1 through 1.4, and it means the version you construct at startup determines which functions are safe to call for the lifetime of the device. The readme also notes that custom loaders can be implemented, so the default is a choice rather than a hard-wiring, and that a project which wants lazy loading or a different failure policy can supply its own.

## Newtyped handles with a documented way to throw the type away

Every Vulkan handle is exposed as its own newtype struct, which is the classic way to stop yourself from passing a device handle where a buffer handle belongs. The readme explains both how to construct the invalid value and how to leave the type system when you need to. A null handle has a named constructor rather than a default-initialised zero value, so the invalid case is something you wrote on purpose. And handles convert freely to and from a 64-bit integer through a trait, named in the readme, and the stated reason is interoperability with Vulkan code that is not this crate. That is the right call and it is worth reading as a design statement rather than a hole. The type safety exists to catch mistakes in code that uses this crate; code that talks to another Vulkan binding, or to a C API, has already crossed the boundary and needs the raw value. The same philosophy appears in the raw function pointer accessors, which the readme offers as the escape for anything the higher-level API has not exposed, with an invitation to open an issue when something is missing, and extension modules are namespaced by vendor prefix, so a swapchain loader is created by naming the instance and the device: 

```rust
use ash::khr;
let swapchain_loader = khr::swapchain::Device::new(&instance, &device);
```

The same prefix scheme is what gives you extension names as constants, so the string for a surface extension is a field on the module rather than a literal you retype. The practical note is that a newtype over a handle is only as good as the discipline around it, and the discipline here is documented rather than assumed, which is the part you should read before you adopt it.

## The video bindings are exempt from semver, and the reason is the generator

There is a warning section in this readme, and it is the kind of warning that saves an upgrade. The Vulkan video bindings are described as experimental, as still seeing breaking changes in their upstream specification, and as provided only for early adopters. The consequence is spelled out: all related functions and types are semver-exempt, and the project allows breaking API changes while releasing non-breaking version bumps. If you depend on the video bindings, a patch release can break your build, and the readme is telling you so rather than leaving you to discover it. What makes the note more than boilerplate is the footnote explaining why the exemption is necessary. The bindings cannot easily be hidden behind a feature flag that is off by default, and that is attributed to generator complexity, with the additional observation that the bindings are widespread across the generated codebase. In other words, the code generator emits them inline rather than in a separate module that could be gated, so there is no cheap way to isolate them. Once you accept that, the rest of the picture is coherent. The crate is generated from the registry, which is where the fidelity comes from, and the cost of generation is that some curation decisions are made by the generator's shape rather than by hand. If you do not touch the video extensions, the exemption does not affect you, and the readme is careful to scope the warning to those who do.

## The generator is a workspace member, next to the code it produces

The workspace manifest lists four members, and one of them changes how you should read the repository. There is the main crate, an examples crate, a separate windowing crate, and a generator. The generator being in the same repository as the output is the significant fact, because it means the mapping from the registry to the Rust API is part of the source you can read, modify and test, rather than an opaque step someone else runs. The readme says the code is generated from the registry XML, and the presence of the generator member is the concrete form of that claim. The practical implications run in both directions. If the generated API is not what you need, you are not stuck patching generated files, because you can change the generator and regenerate. And if you are evaluating the crate's maintenance, you can see how much of it is mechanical and how much is hand-written, which tells you what a maintainer's time actually goes on. The windowing crate is the other member worth noting, because it is a separate published artefact with its own version, released alongside the main crate on the same day in the visible history. The two release lines are independent and you may be pinning one without the other, so treat them as two dependencies rather than one.

## Two licences, two badges, and a version floor that is the real contract

The header carries two licence badges, one for each of the permissive licences, and the repository has two licence files, one for the MIT terms and one for the Apache terms. The manifest records the Apache identifier. Dual licensing under those two is a common and generous arrangement, and the two badges together communicate the choice rather than contradicting it, but the readme does not have a licence section that says so in words, so the badges are the only place a reader learns it. If your organisation's tooling reads a single licence field, it will see one of the two and miss the alternative. That is a small thing to verify once. The more consequential line in the header is the minimum supported Rust version, which is stated as a specific compiler release and linked to the announcement post for it. For a generated crate, that number is the compatibility contract that matters, not the release date, because the code is emitted by a generator and its output has to compile on the oldest toolchain the project supports. The visible release history is consistent with that being treated seriously: the main crate and the windowing crate were both published on the same day in April 2024, and the release before that was in mid-2023. So the project moves on the schedule of a generated API that has to be regenerated when the registry changes, which is the right cadence for this kind of wrapper and a slower one than a hand-written library would keep.

## Conclusion

Adopt ash if you are writing Rust against Vulkan and want the registry's own structure rules enforced by the compiler, because the pointer chain check and the newtyped handles remove a class of mistake that the C API cannot catch, and the raw conversion path is documented for the cases where you must interoperate with other Vulkan code. Do not adopt it expecting a safe abstraction, because the project states plainly that there is no validation and everything is unsafe, and the only guarantee it offers is lifetime-safety on structs created with the builder pattern. Two things to check before you depend on it. Read the semver warning, because the video bindings are exempt and a patch release can break you there, and the stated reason is generator complexity rather than a policy choice. And check the minimum supported Rust version in the manifest against your toolchain, because that number, not the release date, is the compatibility contract you are signing up for.

## FAQ

### Is ash safe to use?

The feature list states there is no validation and everything is unsafe, and the project describes the goal as the true Vulkan API without compromises. The safety it adds is lifetime-safety on structs created with the builder pattern and type-level checking of the extension pointer chain, so treat it as a typed interface to an unsafe C library rather than a safe abstraction.

### What happens if I call a Vulkan 1.1 function on a 1.0 instance in ash?

The readme states that everything is loaded by default and that functions which fail to load are initialised to a function that always panics, so calling a 1.1 function when you created a 1.0 instance results in a panic. The version you create at startup determines which functions are safe to call.

### Why are the Vulkan video bindings in ash exempt from semver?

The readme says the video bindings are experimental, still seeing breaking changes upstream, and that all related functions and types are semver-exempt, so breaking API changes can ship with non-breaking version bumps. The stated reason is that generator complexity makes it impractical to hide the bindings behind a feature flag that is off by default.

### How does ash prevent adding an invalid struct to a create-info chain?

The method that inserts a struct into the pointer chain only accepts structs the registry lists as able to extend the receiver, mapped onto generated marker traits, and only structs appearing in that list implement the method at all. An unsafe variant exists for pushing a struct that already has its own valid chain.

### Which Vulkan versions does ash support and what Rust version does it need?

The feature list names support for Vulkan 1.1, 1.2, 1.3 and 1.4, plus no_std support. The minimum supported Rust version is stated in the readme header as a specific compiler release, and that number rather than the release date is the compatibility contract for a generated crate.

## Sources

- [ash-rs/ash on GitHub](https://github.com/ash-rs/ash)
- [Issues](https://github.com/ash-rs/ash/issues)
- [License: Apache-2.0](https://github.com/ash-rs/ash/blob/master/LICENSE)
- [README](https://github.com/ash-rs/ash/blob/master/README.md)
- [Releases](https://github.com/ash-rs/ash/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/ash-rs-ash
