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 (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
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.
| 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
= "0.1"
# stable, plus the TryReserveError conversion
# (needs a #[global_allocator] in the final artifact)
= { = "0.1", = ["alloc"] }
# nightly, everything
= { = "0.1", = ["alloc", "allocator-api"] }
Core Types
Result<T>
pub type Result<T, E = Error> = Result;
Error
NTSTATUS);
Debug and Display are hand-written rather than derived — see
Formatting.
Status
;
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
use IntoResult;
use ;
// Stand-in for a kernel API returning a raw status.
assert!;
| 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.
Returning NTSTATUS from Result
use NtStatus;
use STATUS_SUCCESS;
assert_eq!;
| ntresult::Result<()> | NTSTATUS |
|---|---|
| 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.
use NtStatusOrSuccess;
use STATUS_SUCCESS;
// The payload is discarded; only success or the error's status remains.
assert_eq!;
| ntresult::Result | NTSTATUS |
|---|---|
| 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:
use ;
use STATUS_BUFFER_TOO_SMALL;
// The carried status survives; it is not collapsed to STATUS_SUCCESS.
assert_eq!;
| 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.
Inspecting a status
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 ;
use STATUS_ACPI_INVALID_DATA;
// STATUS_ACPI_INVALID_DATA is 0xC014000F
let err = from_ntstatus;
assert_eq!;
assert_eq!;
assert_eq!;
assert!;
assert!;
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.
use Error;
// `from_bits` takes the unsigned form, so no `as i32` cast is needed.
let mine = from_bits;
assert!;
assert!;
assert_eq!;
Error Conversion
To turn an NTSTATUS straight into an Error, use the IntoError trait.
use IntoError;
use STATUS_INVALID_PARAMETER;
assert!;
assert!;
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:
| 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.
Formatting
Error and Status both implement Display and Debug, rendering the status
as 0x plus eight uppercase hexadecimal digits.
use Error;
use STATUS_ACCESS_DENIED;
let err = from_ntstatus;
assert_eq!; // Display
assert_eq!; // 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.
| 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 ;
use ;
// Stand-in for a kernel API returning a raw status.
assert!;
assert!;
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 or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license (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.