Skip to content

feat: stack-based allocator for wdk crate #611

Open
leon-xd wants to merge 19 commits intomicrosoft:mainfrom
leon-xd:generic-stack-alloc
Open

feat: stack-based allocator for wdk crate #611
leon-xd wants to merge 19 commits intomicrosoft:mainfrom
leon-xd:generic-stack-alloc

Conversation

@leon-xd
Copy link
Contributor

@leon-xd leon-xd commented Feb 13, 2026

Adds and integrates wdk::fmt::WdkFormatBuffer.

Summary

  • Introduced crates/wdk/src/fmt.rs with a fixed-size, stack-friendly formatting buffer (WdkFormatBuffer<T>) implementing fmt::Write.
  • Added methods to borrow buffer as core::ffi::CStr and &str alongside tests to ensure correctness.
  • Re-exported WdkFormatBuffer from wdk::fmt via lib.rs for WDM/KMDF/UMDF driver models.
  • [BREAKING CHANGE] Swapped print.rs kernel path to use WdkFormatBuffer instead of DbgPrintBufWriter; removed that module and its tests.

Details

  • WdkFormatBuffer<T = 512> stores a zero-initialized [u8; T] and tracks used bytes. Default delegates to new() for convenience.
  • as_str() returns a UTF-8 view over written bytes only.
  • as_cstr() ensures a trailing NUL when the buffer is filled; returns FromBytesUntilNulError on missing terminator (e.g., T == 0).
  • fmt::Write implementation clamps writes to capacity and signals truncation via fmt::Error.
  • print.rs kernel path now formats into WdkFormatBuffer and calls DbgPrint directly using the resulting CStr. The previous dbg_print_buf_writer module and its flush logic/tests were removed. This results in different behavior when handling overflowing buffers.
  • Added comprehensive tests covering:
    • basic write and as_str()/as_cstr() usage
    • overflow and multi-write scenarios
    • exact-fit writes, empty writes, and zero-sized buffers
    • reference borrowing sanity checks

Rationale

  • Provides a heap-free formatting utility suitable for driver environments.
  • Exposing via wdk::fmt (and pub use fmt::WdkFormatBuffer) removes dead-code warnings and makes the type available to consumers.

Testing

cargo test --package wdk --lib --all-features -- fmt::test --nocapture

Notes / Follow-ups

  • We currently ignore fmt::write errors and silently return on as_cstr failure. A future improvement could be a chunked/flush API on WdkFormatBuffer to mimic prior semantics.

Copilot AI review requested due to automatic review settings February 13, 2026 00:10
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a new fixed-size, stack-friendly formatting buffer (WdkFormatBuffer) to support heap-free formatting in driver contexts, and integrates it into the kernel print!/println! path by replacing the previous DbgPrintBufWriter implementation.

Changes:

  • Added crates/wdk/src/fmt.rs implementing WdkFormatBuffer<const T: usize = 512> with fmt::Write, plus as_str() / as_cstr() helpers and tests.
  • Updated crates/wdk/src/print.rs (WDM/KMDF path) to format into WdkFormatBuffer and call DbgPrint directly; removed the old buffering/flushing module.
  • Adjusted the KMDF sample driver to add an explicit type annotation for the WdfDriverCreate output handle pointer.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 7 comments.

File Description
examples/sample-kmdf-driver/src/lib.rs Tweaks the WdfDriverCreate out-handle variable typing.
crates/wdk/src/print.rs Switches kernel printing to WdkFormatBuffer + direct DbgPrint.
crates/wdk/src/lib.rs Adds the new fmt module and re-exports WdkFormatBuffer.
crates/wdk/src/fmt.rs New formatting buffer type, CStr/str views, and unit tests.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@codecov-commenter
Copy link

codecov-commenter commented Feb 13, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 77.41%. Comparing base (009dcd7) to head (bc4e1e3).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #611   +/-   ##
=======================================
  Coverage   77.41%   77.41%           
=======================================
  Files          24       24           
  Lines        4853     4853           
  Branches     4853     4853           
=======================================
  Hits         3757     3757           
  Misses        979      979           
  Partials      117      117           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@leon-xd leon-xd changed the title feat!: stack-based allocator for WDK crate feat!: stack-based allocator for wdk crate Feb 13, 2026
Copilot AI review requested due to automatic review settings February 19, 2026 19:04
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@leon-xd leon-xd force-pushed the generic-stack-alloc branch from 8050685 to 74a4e88 Compare February 19, 2026 19:10
Copilot AI review requested due to automatic review settings February 19, 2026 19:42
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@leon-xd leon-xd marked this pull request as ready for review February 19, 2026 22:29
Copilot AI review requested due to automatic review settings February 25, 2026 22:34
@leon-xd
Copy link
Contributor Author

leon-xd commented Feb 26, 2026

@leon-xd is there a reason that you removed the buffered printing and flushing functionality?

@wmmc88 Thought about this for a bit and realized I should just prevent the breaking change if possible. So added a new type, WdkFlushableFormatBuffer as a wrapper around WdkFormatBuffer that includes a flush function that the user defines. Print should work as it did before, making this PR not contain a breaking change. Adjusting the PR title accordingly.

@leon-xd leon-xd changed the title feat!: stack-based allocator for wdk crate feat: stack-based allocator for wdk crate Feb 26, 2026
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +40 to +47
mod fmt;

#[cfg(any(
driver_model__driver_type = "WDM",
driver_model__driver_type = "KMDF",
driver_model__driver_type = "UMDF"
))]
pub use fmt::{WdkFlushableFormatBuffer, WdkFormatBuffer};
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This module is declared as mod fmt; (private), so external users cannot access the wdk::fmt::... path described in the PR (and earlier discussion). If the intended public API is wdk::fmt::WdkFormatBuffer, make this pub mod fmt (or re-export a pub mod fmt { pub use super::fmt::*; }). Otherwise, update the PR description/docs to reflect the actual public path (wdk::WdkFormatBuffer).

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'm comfortable just exporting it as wdk::WdkFormatBuffer and wdk::WdkFlushableFormatBufer. not sure if anyone else has any opinions

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i agree with copilot. should reduce the duplication and its typically convention to do stuff like wdk::FormatBuffer

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Leon Durrenberger <leon.durrenberger@gmail.com>
Copilot AI review requested due to automatic review settings February 27, 2026 22:01
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +161 to +186
impl<F: FnMut(&WdkFormatBuffer<N>), const N: usize> fmt::Write for WdkFlushableFormatBuffer<F, N> {
fn write_str(&mut self, s: &str) -> fmt::Result {
let capacity = N - 1;
let mut remaining = &s.as_bytes()[..];

// Fill what fits, flush, continue with the rest.
while remaining.len() > capacity - self.format_buffer.used {
let space = capacity - self.format_buffer.used;
self.format_buffer.buffer[self.format_buffer.used..self.format_buffer.used + space]
.copy_from_slice(&remaining[..space]);
self.format_buffer.used = capacity;

(self.flush_fn)(&self.format_buffer);
self.format_buffer.reset();

remaining = &remaining[space..];
}

// Remaining bytes fit in the buffer.
self.format_buffer.buffer
[self.format_buffer.used..self.format_buffer.used + remaining.len()]
.copy_from_slice(remaining);
self.format_buffer.used += remaining.len();
Ok(())
}
}
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WdkFlushableFormatBuffer::write_str splits the incoming &str by raw bytes when chunking, which can cut through a multi-byte UTF-8 codepoint. That makes WdkFormatBuffer::as_str() potentially return Err(Utf8Error) for a flushed chunk even though the original formatted output was valid UTF-8. Either document this explicitly (recommended, since as_str is fallible) or change the chunking logic to split on UTF-8 boundaries when the flush consumer is expected to treat chunks as &str.

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@leon-xd Copilot is right. We should end at a clean char boundary not a byte boundary. as_str returning an error is not the only problem. A bigger problem would be DbgPrint emitting garbled output if the last char gets cut in the middle.

We should also add a test for this.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, i think this should be chunking off of graphemes

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally it should be graphemes not chars, but that will require pulling in the unicode_segmentation crate and might increase binary size a bit as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I ended up chunking based off code points, not graphemes. Hope that's an ok middle ground? Otherwise I can try pulling in the unicode_segmentation crate or adding some functionality myself. Let me know @gurry @wmmc88

Comment on lines +161 to +186
impl<F: FnMut(&WdkFormatBuffer<N>), const N: usize> fmt::Write for WdkFlushableFormatBuffer<F, N> {
fn write_str(&mut self, s: &str) -> fmt::Result {
let capacity = N - 1;
let mut remaining = &s.as_bytes()[..];

// Fill what fits, flush, continue with the rest.
while remaining.len() > capacity - self.format_buffer.used {
let space = capacity - self.format_buffer.used;
self.format_buffer.buffer[self.format_buffer.used..self.format_buffer.used + space]
.copy_from_slice(&remaining[..space]);
self.format_buffer.used = capacity;

(self.flush_fn)(&self.format_buffer);
self.format_buffer.reset();

remaining = &remaining[space..];
}

// Remaining bytes fit in the buffer.
self.format_buffer.buffer
[self.format_buffer.used..self.format_buffer.used + remaining.len()]
.copy_from_slice(remaining);
self.format_buffer.used += remaining.len();
Ok(())
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@leon-xd Copilot is right. We should end at a clean char boundary not a byte boundary. as_str returning an error is not the only problem. A bigger problem would be DbgPrint emitting garbled output if the last char gets cut in the middle.

We should also add a test for this.


/// Resets the buffer to its initial empty state.
pub fn reset(&mut self) {
self.buffer = [0; N];
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

open question: should this even bother to zero out the buffer since the buffer is private?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no need to zero out.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

after a flush the format buffer calls reset. i'd like to keep it if possible?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i see -- nothing gets beyond used so it doesn't really matter what's in the rest of the buffer. removed.

///
/// The buffer always contains a NUL terminator because `write_str`
/// reserves the last byte.
pub const fn as_cstr(&self) -> &CStr {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what happens when we have internal null since that's valid in rust string?

Copy link
Contributor

@gurry gurry Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could skip any embedded nulls in the incoming string when writing to the buffer. Or replace them with ?s.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it writes until the first null, I believe this is documented

Comment on lines +161 to +186
impl<F: FnMut(&WdkFormatBuffer<N>), const N: usize> fmt::Write for WdkFlushableFormatBuffer<F, N> {
fn write_str(&mut self, s: &str) -> fmt::Result {
let capacity = N - 1;
let mut remaining = &s.as_bytes()[..];

// Fill what fits, flush, continue with the rest.
while remaining.len() > capacity - self.format_buffer.used {
let space = capacity - self.format_buffer.used;
self.format_buffer.buffer[self.format_buffer.used..self.format_buffer.used + space]
.copy_from_slice(&remaining[..space]);
self.format_buffer.used = capacity;

(self.flush_fn)(&self.format_buffer);
self.format_buffer.reset();

remaining = &remaining[space..];
}

// Remaining bytes fit in the buffer.
self.format_buffer.buffer
[self.format_buffer.used..self.format_buffer.used + remaining.len()]
.copy_from_slice(remaining);
self.format_buffer.used += remaining.len();
Ok(())
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, i think this should be chunking off of graphemes

/// # Errors
/// Returns an error if the written bytes are not valid UTF-8.
pub fn as_str(&self) -> Result<&str, Utf8Error> {
core::str::from_utf8(&self.buffer[..self.used])
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is guaranteed to always succeed

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +49 to +55
pub const fn new() -> Self {
const {
assert!(
N >= 2,
"N must be at least 2 (one byte of content plus the NUL terminator)"
)
}
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR description mentions support/tests for zero-sized buffers and as_cstr() returning an error when a terminator is missing (e.g., N == 0), but the implementation now enforces N >= 2 at compile time and as_cstr() panics on the error path. Please update the PR description (or relax the N >= 2 constraint / make as_cstr fallible) so the documented behavior matches the code.

Copilot uses AI. Check for mistakes.
Comment on lines +157 to +158
/// Interior NUL bytes in the formatted output will cause the string to be
/// truncated at the first NUL.
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_print's doc comment says interior NUL bytes will truncate output at the first NUL, but the UMDF implementation explicitly splits on the first NUL, prints the prefix, then recursively prints the remaining content (i.e., it strips NULs rather than truncating). Please update this doc comment to reflect the UMDF behavior (or make the implementation match the truncation behavior).

Suggested change
/// Interior NUL bytes in the formatted output will cause the string to be
/// truncated at the first NUL.
/// Interior NUL bytes in the formatted output are handled differently depending
/// on the driver model:
/// - In WDM/KMDF builds, each chunk is passed to `DbgPrint` as a C string, so the
/// output is effectively truncated at the first NUL byte in each chunk.
/// - In UMDF builds, the implementation splits on NUL bytes and logs each
/// NUL-separated segment in sequence, effectively stripping the NUL bytes from
/// the output.

Copilot uses AI. Check for mistakes.
Comment on lines +35 to +47
#[cfg(any(
driver_model__driver_type = "WDM",
driver_model__driver_type = "KMDF",
driver_model__driver_type = "UMDF"
))]
mod fmt;

#[cfg(any(
driver_model__driver_type = "WDM",
driver_model__driver_type = "KMDF",
driver_model__driver_type = "UMDF"
))]
pub use fmt::{WdkFlushableFormatBuffer, WdkFormatBuffer};
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do these need to be gated?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants