-
Notifications
You must be signed in to change notification settings - Fork 119
feat!: stack-based allocator for wdk crate
#611
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this 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.rsimplementingWdkFormatBuffer<const T: usize = 512>withfmt::Write, plusas_str()/as_cstr()helpers and tests. - Updated
crates/wdk/src/print.rs(WDM/KMDF path) to format intoWdkFormatBufferand callDbgPrintdirectly; removed the old buffering/flushing module. - Adjusted the KMDF sample driver to add an explicit type annotation for the
WdfDriverCreateoutput 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.
| CStr::from_bytes_until_nul(&self.buffer) | ||
| // PR comments -- I tried to make sure that my implementation never | ||
| // passed a FromBytesUntilNulError, and the function just returned a | ||
| // &CStr. But handling this error here caused a panic which | ||
| // feels too strict given the driver environment since that would cause | ||
| // a bugcheck for kernel drivers. Determined that the most | ||
| // elegant way was for the user to always handle the errors but I'm open | ||
| // to feedback here. | ||
| // | ||
| // // if I do implement this way how should I handle an error case here? | ||
| // feels too strong to panic but this case cannot happen as the current | ||
| // logic stands. if cstr.is_err() { | ||
| // unreachable!("Buffer should always contain a trailing null | ||
| // byte"); } | ||
| // cstr.unwrap_or_else(| _err | { | ||
| // // SAFETY: We pass a single null byte into | ||
| // `from_bytes_with_nul_unchecked`. This means that it is null | ||
| // terminated and has no interior null bytes. unsafe{ | ||
| // CStr::from_bytes_with_nul_unchecked(b"\0") | ||
| // } | ||
| // }) | ||
| } |
Copilot
AI
Feb 13, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
as_cstr contains a large block of PR discussion and commented-out code after the return expression. This will ship as part of the crate source and makes the method hard to read/maintain; it should be removed or rewritten as a concise doc comment explaining the chosen behavior and invariants.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will remove this after getting comments from a human :)
| /// Returns a C string view up to the first `NUL` byte. | ||
| /// | ||
| /// Ensures termination by writing a `NUL` if the buffer is completely | ||
| /// filled. | ||
| /// | ||
| /// # Errors | ||
| /// Returns an error only if no terminator is found, e.g. if `T == 0`. | ||
| pub const fn as_cstr(&mut self) -> Result<&CStr, FromBytesUntilNulError> { | ||
| if self.used == T && T != 0 { | ||
| self.buffer[self.used - 1] = 0; | ||
| } | ||
| CStr::from_bytes_until_nul(&self.buffer) |
Copilot
AI
Feb 13, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
as_cstr mutates the buffer when used == T by overwriting the last written byte with NUL, which silently drops one byte of output and leaves used unchanged (so a subsequent as_str() call can include a trailing \0 and miss the last character). Consider reserving space for the terminator up-front (cap writes at T-1 and keep buffer[T-1] == 0), or have as_cstr() return an error when the contents cannot be represented as a C string without truncation.
| /// Returns a C string view up to the first `NUL` byte. | |
| /// | |
| /// Ensures termination by writing a `NUL` if the buffer is completely | |
| /// filled. | |
| /// | |
| /// # Errors | |
| /// Returns an error only if no terminator is found, e.g. if `T == 0`. | |
| pub const fn as_cstr(&mut self) -> Result<&CStr, FromBytesUntilNulError> { | |
| if self.used == T && T != 0 { | |
| self.buffer[self.used - 1] = 0; | |
| } | |
| CStr::from_bytes_until_nul(&self.buffer) | |
| /// Returns a C string view over the written bytes, up to the first `NUL`. | |
| /// | |
| /// This method does not modify the buffer. It inspects only the portion of | |
| /// the buffer that has been written (`used` bytes) when searching for the | |
| /// terminator. | |
| /// | |
| /// # Errors | |
| /// Returns an error if no terminator is found in the written bytes (for | |
| /// example, if `used == 0` or if the contents contain no `NUL`). | |
| pub const fn as_cstr(&mut self) -> Result<&CStr, FromBytesUntilNulError> { | |
| CStr::from_bytes_until_nul(&self.buffer[..self.used]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah fair, I'll wait for human input for this one. want to balance the expectations of having Rust-only strings, but given the developer experience required to build in WDR this might be over compensating for a non-helpful use case. had a comment somewhere about this design decision.
| impl<const T: usize> fmt::Write for WdkFormatBuffer<T> { | ||
| fn write_str(&mut self, s: &str) -> fmt::Result { | ||
| if s.len() + self.used >= T { | ||
| self.buffer[self.used..T].copy_from_slice(&s.as_bytes()[..T - self.used]); | ||
| self.used = T; | ||
| return Err(fmt::Error); | ||
| } | ||
| self.buffer[self.used..self.used + s.len()].copy_from_slice(s.as_bytes()); | ||
| self.used += s.len(); | ||
| Ok(()) |
Copilot
AI
Feb 13, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fmt::Write::write_str returns Err(fmt::Error) when s.len() + used == T even though it copies the entire string into the buffer. That makes exact-fit writes look like failures (e.g., write!(&mut buf, ...) will return Err/panic on unwrap() even though no truncation occurred for as_str). Align the boundary/return semantics with the intended capacity rules (e.g., allow exact-fit for as_str, or clearly reserve T-1 for C strings and only return Err when bytes are actually dropped).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is reflected in my test cases as ideal behavior since converting to cstr will truncate. just need feedback on design before making any changes.
| let mut buffered_writer: WdkFormatBuffer = WdkFormatBuffer::new(); | ||
| // We do not care whether this is `Ok` or `Err` right now. If `Err` will simply write all until overflow. | ||
| // TODO: create custom `flush` param in `WdkFormatBuffer` to preserve old functionality | ||
| let _ = fmt::write(&mut buffered_writer, args); | ||
|
|
||
| let cstr_buffer = match buffered_writer.as_cstr() { | ||
| Ok(cstr) => cstr, | ||
| Err(_e) => return, // silently return on error, no null terminator. (Should this be a placeholder string for debugging purposes?) | ||
| }; | ||
|
|
||
| if fmt::write(&mut buffered_writer, args).is_ok() { | ||
| buffered_writer.flush(); | ||
| } else { | ||
| unreachable!("DbgPrintBufWriter should never fail to write"); | ||
| unsafe { | ||
| wdk_sys::ntddk::DbgPrint( | ||
| c"%s".as_ptr().cast(), | ||
| cstr_buffer.as_ptr().cast::<wdk_sys::CHAR>(), | ||
| ); | ||
| } |
Copilot
AI
Feb 13, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The WDM/KMDF docstrings above print!/println! still claim the 512-byte DbgPrint limit "does not apply" because output is chunked, but the new implementation formats once into a fixed buffer and calls DbgPrint a single time (overflow is truncated). Also, _print is documented as panicking on interior NULs, but the new CStr-based path will just stop at the first NUL. Please update the docs (and/or implementation) so behavior matches the documented guarantees.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
great point!
| #[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::WdkFormatBuffer; | ||
|
|
Copilot
AI
Feb 13, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fmt is declared as a private module (mod fmt;), but the docs/PR description reference wdk::fmt::WdkFormatBuffer. As-is, downstream users cannot access wdk::fmt::* and the doctest path in fmt.rs will not compile. Make the module public (pub mod fmt;) and optionally keep the root pub use, or update the docs/exports to match the intended public path.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #611 +/- ##
==========================================
+ Coverage 76.52% 77.41% +0.89%
==========================================
Files 24 24
Lines 4881 4853 -28
Branches 4881 4853 -28
==========================================
+ Hits 3735 3757 +22
+ Misses 1029 979 -50
Partials 117 117 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
wdk crate
Adds and integrates
wdk::fmt::WdkFormatBuffer.Summary
crates/wdk/src/fmt.rswith a fixed-size, stack-friendly formatting buffer (WdkFormatBuffer<T>) implementingfmt::Write.core::ffi::CStrand&stralongside tests to ensure correctness.WdkFormatBufferfromwdk::fmtvialib.rsfor WDM/KMDF/UMDF driver models.print.rskernel path to useWdkFormatBufferinstead ofDbgPrintBufWriter; removed that module and its tests.Details
WdkFormatBuffer<T = 512>stores a zero-initialized[u8; T]and tracksusedbytes.Defaultdelegates tonew()for convenience.as_str()returns a UTF-8 view over written bytes only.as_cstr()ensures a trailing NUL when the buffer is filled; returnsFromBytesUntilNulErroron missing terminator (e.g.,T == 0).fmt::Writeimplementation clamps writes to capacity and signals truncation viafmt::Error.print.rskernel path now formats intoWdkFormatBufferand callsDbgPrintdirectly using the resultingCStr. The previousdbg_print_buf_writermodule and its flush logic/tests were removed. This results in different behavior when handling overflowing buffers.as_str()/as_cstr()usageRationale
wdk::fmt(andpub use fmt::WdkFormatBuffer) removes dead-code warnings and makes the type available to consumers.Testing
Notes / Follow-ups
fmt::writeerrors and silently return onas_cstrfailure. A future improvement could be a chunked/flush API onWdkFormatBufferto mimic prior semantics.