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.
#![no_std]support (optionalalloc)- Builds on stable Rust (nightly only for the opt-in
allocator-apifeature) - Zero-cost abstraction over
NTSTATUS - Transparent
ErrorandStatuswrappers - 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
unsafecode
All features are additive: each one only adds a From conversion into Error.
Enabling a feature never changes the behaviour of an existing conversion.
| Feature | Default | Toolchain | Conversion added |
|---|---|---|---|
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.
# 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"] }pub type Result<T, E = Error> = core::result::Result<T, E>;#[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.
#[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.
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());| NTSTATUS | ntresult::Result<()> |
|---|---|
| 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.
use ntresult::NtStatus;
use windows_sys::Win32::Foundation::STATUS_SUCCESS;
fn driver_fn() -> ntresult::Result<()> {
Ok(())
}
assert_eq!(driver_fn().ntstatus(), STATUS_SUCCESS);| ntresult::Result<()> | NTSTATUS |
|---|---|
| Ok(()) | STATUS_SUCCESS |
| Err(Error(status)) | status |
When the Ok payload is data rather than a status, use ntstatus_or_success().
The name states that the payload is discarded.
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);| ntresult::Result | NTSTATUS |
|---|---|
| Ok(T) | STATUS_SUCCESS |
| Err(Error(status)) | status |
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:
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);| StatusResult (= Result) | NTSTATUS |
|---|---|
| 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.
An NTSTATUS is not opaque — it packs four fields:
31 30 | 29 | 28 | 27 ------ 16 | 15 ------- 0
Sev | C | R | Facility | Code
Error and Status expose the same accessors for all of them.
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.
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.
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);To turn an NTSTATUS straight into an Error, use the IntoError trait.
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());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:
| Error type | NTSTATUS |
Feature |
|---|---|---|
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.
Error and Status both implement Display and Debug, rendering the status
as 0x plus eight uppercase hexadecimal digits.
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)"); // DebugDebug matters most in unwrap() panics and assert_eq! output, where a
derived implementation would print Error(-1073741790) instead.
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.
| Macro | Expands to |
|---|---|
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! |
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.
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.
Licensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
at your option.
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.