# ntresult
Lightweight `NTSTATUS`-based error handling for Windows kernel-mode Rust code.
`ntresult` provides a minimal and idiomatic interface for working with Windows `NTSTATUS` values in Rust, designed specifically for `#![no_std]` and kernel-mode environments. It bridges native Windows status codes with Rust's `Result` type without introducing unnecessary abstraction or overhead.
---
## Highlights
- `#![no_std]` support (optional `alloc`)
- Builds on **stable** Rust (nightly only for the opt-in `allocator-api` feature)
- Zero-cost abstraction over `NTSTATUS`
- Transparent `Error` and `Status` wrappers
- Idiomatic `Result`-based API
- Explicit handling of expected status values
- Severity, facility and code inspection
- Shorthand macros for the common conversions
- No memory allocations
- No `unsafe` code
---
## Feature flags
All features are additive: each one only adds a `From` conversion into `Error`.
Enabling a feature never changes the behaviour of an existing conversion.
| `alloc` | no | stable | `alloc::collections::TryReserveError` |
| `allocator-api` | no | **nightly** | `core::alloc::AllocError` |
Conversions that need nothing beyond `core` — `core::num::TryFromIntError` and
`core::net::AddrParseError` — are always available and are not gated behind a feature.
**There are no default features.** Enabling `alloc` links the `alloc` crate, which
makes rustc require a `#[global_allocator]` from the final artifact — a driver, a
`staticlib` — *even if nothing ever allocates*. A driver that only wants the
`NTSTATUS` wrapper should not be made to supply one, so `alloc` is opt-in.
`allocator-api` enables the unstable `allocator_api` language feature and therefore
requires a nightly compiler. Everything else builds on stable Rust.
`allocator-api` does **not** imply `alloc`: `AllocError` is defined in `core`, so a
driver using a custom `Allocator` without a global allocator can still use it.
```toml
# stable, no allocator required
ntresult = "0.1"
# stable, plus the TryReserveError conversion
# (needs a #[global_allocator] in the final artifact)
ntresult = { version = "0.1", features = ["alloc"] }
# nightly, everything
ntresult = { version = "0.1", features = ["alloc", "allocator-api"] }
```
---
## Core Types
### `Result<T>`
```rust,ignore
pub type Result<T, E = Error> = core::result::Result<T, E>;
```
### `Error`
```rust,ignore
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
#[repr(transparent)]
pub struct Error(pub(crate) NTSTATUS);
impl core::error::Error for Error {}
```
`Debug` and `Display` are hand-written rather than derived — see
[Formatting](#formatting).
### `Status`
```rust,ignore
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
#[repr(transparent)]
pub struct Status(NTSTATUS);
```
The same data as `Error`, with the opposite intent: a status that is an expected
outcome rather than a failure. It exposes an identical set of accessors.
## Usage
### Converting NTSTATUS → Result
```rust
use ntresult::IntoResult;
use windows_sys::Win32::Foundation::{NTSTATUS, STATUS_SUCCESS};
// Stand-in for a kernel API returning a raw status.
fn some_kernel_call() -> NTSTATUS {
STATUS_SUCCESS
}
fn my_func() -> ntresult::Result<()> {
some_kernel_call().into_result()
}
assert!(my_func().is_ok());
```
| STATUS_SUCCESS | Ok(()) |
| any other code | Err(Error(NTSTATUS)) |
`ntresult` intentionally treats all non-`STATUS_SUCCESS` values as errors.
This preserves strict semantics and avoids ambiguity.
### Returning NTSTATUS from Result
```rust
use ntresult::NtStatus;
use windows_sys::Win32::Foundation::STATUS_SUCCESS;
fn driver_fn() -> ntresult::Result<()> {
Ok(())
}
assert_eq!(driver_fn().ntstatus(), STATUS_SUCCESS);
```
| Ok(()) | STATUS_SUCCESS |
| Err(Error(status)) | status |
### Returning an arbitrary data type
When the `Ok` payload is data rather than a status, use `ntstatus_or_success()`.
The name states that the payload is discarded.
```rust
use ntresult::NtStatusOrSuccess;
use windows_sys::Win32::Foundation::STATUS_SUCCESS;
fn read_register() -> ntresult::Result<u32> {
Ok(0x1234)
}
// The payload is discarded; only success or the error's status remains.
assert_eq!(read_register().ntstatus_or_success(), STATUS_SUCCESS);
```
| Ok(T) | STATUS_SUCCESS |
| Err(Error(status)) | status |
### Returning expected non-success statuses
Some kernel APIs use non-success `NTSTATUS` values as valid outcomes (e.g. `STATUS_BUFFER_TOO_SMALL`).
Return them as `Ok` in a `StatusResult`, and they survive extraction:
```rust
use ntresult::{NtStatus, Status, StatusResult};
use windows_sys::Win32::Foundation::STATUS_BUFFER_TOO_SMALL;
fn driver_fn() -> StatusResult {
Ok(Status::from_ntstatus(STATUS_BUFFER_TOO_SMALL))
}
// The carried status survives; it is not collapsed to STATUS_SUCCESS.
assert_eq!(driver_fn().ntstatus(), STATUS_BUFFER_TOO_SMALL);
```
| Ok(Status(status)) | status |
| Err(Error(status)) | status |
`Status` is a newtype rather than a bare `NTSTATUS` because `NTSTATUS` is an
alias for `i32`: without it, a `Result<i32>` carrying a byte count would be
indistinguishable from one carrying a status code.
---
## Inspecting a status
An `NTSTATUS` is not opaque — it packs four fields:
```text
```
`Error` and `Status` expose the same accessors for all of them.
```rust
use ntresult::{Error, Severity};
use windows_sys::Win32::Foundation::STATUS_ACPI_INVALID_DATA;
// STATUS_ACPI_INVALID_DATA is 0xC014000F
let err = Error::from_ntstatus(STATUS_ACPI_INVALID_DATA);
assert_eq!(err.severity(), Severity::Error);
assert_eq!(err.facility(), 0x014);
assert_eq!(err.code(), 0x000F);
assert!(err.is_error());
assert!(!err.is_customer());
```
`Severity` is ordered, so `err.severity() >= Severity::Warning` works.
Beware `is_success()`: it reports the *severity field*, not whether the status
equals `STATUS_SUCCESS`. `STATUS_PENDING` and `STATUS_TIMEOUT` both have Success
severity while still being errors as far as `into_result()` is concerned.
### Customer-defined status codes
Set bit 29 to mint your own codes — Microsoft guarantees it will never define a
status with that bit set, so yours can never collide. Customer values are
recognisable by their leading nibble: `0x2` success, `0x6` informational,
`0xA` warning, `0xE` error.
```rust
use ntresult::Error;
// `from_bits` takes the unsigned form, so no `as i32` cast is needed.
let mine = Error::from_bits(0xE000_0001);
assert!(mine.is_customer());
assert!(mine.is_error());
assert_eq!(mine.code(), 0x0001);
```
---
## Error Conversion
To turn an `NTSTATUS` straight into an `Error`, use the `IntoError` trait.
```rust
use ntresult::IntoError;
use windows_sys::Win32::Foundation::STATUS_INVALID_PARAMETER;
fn check_len(len: usize) -> ntresult::Result<()> {
if len == 0 {
return Err(STATUS_INVALID_PARAMETER.into_error());
}
Ok(())
}
assert!(check_len(0).is_err());
assert!(check_len(1).is_ok());
```
### Common error types
The `common_error` module provides conversions from common Rust errors into
`ntresult::Error`, so a Rust-level failure can propagate through `?` and be
returned to the kernel as a status code. Each error maps to the `NTSTATUS`
that best describes it:
| `core::num::TryFromIntError` | `STATUS_INTEGER_OVERFLOW` | always |
| `core::net::AddrParseError` | `STATUS_INVALID_ADDRESS` | always |
| `alloc::collections::TryReserveError` | `STATUS_INSUFFICIENT_RESOURCES` | `alloc` |
| `core::alloc::AllocError` | `STATUS_INSUFFICIENT_RESOURCES` | `allocator-api` |
More types will be added in the future.
### Formatting
`Error` and `Status` both implement `Display` and `Debug`, rendering the status
as `0x` plus eight uppercase hexadecimal digits.
```rust
use ntresult::Error;
use windows_sys::Win32::Foundation::STATUS_ACCESS_DENIED;
let err = Error::from_ntstatus(STATUS_ACCESS_DENIED);
assert_eq!(err.to_string(), "0xC0000022"); // Display
assert_eq!(format!("{err:?}"), "Error(0xC0000022)"); // Debug
```
`Debug` matters most in `unwrap()` panics and `assert_eq!` output, where a
derived implementation would print `Error(-1073741790)` instead.
---
## Macros
Shorthands for the conversions above. Each has a `_ret` variant that returns
immediately, which is what makes them worth having in driver code full of early
exits.
| `ntok!(v)` | `Ok(v)` |
| `nterr!(s)` | `Err(Error::from_ntstatus(s))` |
| `ntres!(s)` | `s.into_result()` — `Ok(())` on `STATUS_SUCCESS`, else `Err` |
| `ntok_ret!(v)` | `return Ok(v)` |
| `nterr_ret!(s)` | `return Err(Error::from_ntstatus(s))` |
| `ntres_ret!(s)` | `return s.into_result()` |
| `ntbail!(s)` | alias for `nterr_ret!` |
```rust
use ntresult::{ntres, nterr_ret};
use windows_sys::Win32::Foundation::{
NTSTATUS, STATUS_INVALID_PARAMETER, STATUS_SUCCESS,
};
// Stand-in for a kernel API returning a raw status.
fn kernel_call() -> NTSTATUS {
STATUS_SUCCESS
}
fn init(len: usize) -> ntresult::Result<()> {
if len == 0 {
// return Err(Error::from_ntstatus(...)) in one step
nterr_ret!(STATUS_INVALID_PARAMETER);
}
// NTSTATUS -> Result, then propagate with `?`
ntres!(kernel_call())?;
Ok(())
}
assert!(init(0).is_err());
assert!(init(4).is_ok());
```
`ntok!` and `ntok_ret!` perform no conversion — they are plain `Ok(..)` and
`return Ok(..)`, provided so the family reads consistently.
`ntbail!` is the same macro as `nterr_ret!` under the name used by `anyhow` and
similar crates; pick whichever reads better where you are.
---
## Minimum supported Rust version
**1.85**, set by the 2024 edition. The `allocator-api` feature additionally
requires a nightly compiler; everything else builds on stable.
The MSRV is treated as a compatibility promise: raising it is a breaking change
and will come with a minor version bump before 1.0.
## License
Licensed under either of
- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or
<http://www.apache.org/licenses/LICENSE-2.0>)
- MIT license ([LICENSE-MIT](LICENSE-MIT) or
<http://opensource.org/licenses/MIT>)
at your option.
### Contribution
Unless you explicitly state otherwise, any contribution intentionally submitted
for inclusion in the work by you, as defined in the Apache-2.0 license, shall be
dual licensed as above, without any additional terms or conditions.