From 88d3948e493e9f9eea1c83bdb0ef8d831e33ed59 Mon Sep 17 00:00:00 2001 From: Alan Ngo Date: Thu, 18 Jun 2026 11:29:31 -0700 Subject: [PATCH 01/10] vhf library linker directive redesign, pull out static library linker directives from wdk-build/src/lib.rs into wdk-sys/build.rs --- crates/wdk-build/src/lib.rs | 36 +------ crates/wdk-sys/build.rs | 205 +++++++++++++++++++++++++++++++++++- 2 files changed, 204 insertions(+), 37 deletions(-) diff --git a/crates/wdk-build/src/lib.rs b/crates/wdk-build/src/lib.rs index 710708e5b..a503cb7ac 100644 --- a/crates/wdk-build/src/lib.rs +++ b/crates/wdk-build/src/lib.rs @@ -1125,20 +1125,12 @@ impl Config { println!("cargo::rustc-link-search={}", path.display()); } + // TODO: Once [link-arg-attribute](https://doc.rust-lang.org/unstable-book/language-features/link-arg-attribute.html) + // stabilizes, the `cargo::rustc-cdylib-link-arg=*` directives will be moved to + // the wdk-sys build script match &self.driver_config { DriverConfig::Wdm => { // Emit WDM-specific libraries to link to - println!("cargo::rustc-link-lib=static=BufferOverflowFastFailK"); - println!("cargo::rustc-link-lib=static=ntoskrnl"); - println!("cargo::rustc-link-lib=static=hal"); - println!("cargo::rustc-link-lib=static=wmilib"); - - // Emit ARM64-specific libraries to link to derived from - // WindowsDriver.arm64.props - if self.cpu_architecture == CpuArchitecture::Arm64 { - println!("cargo::rustc-link-lib=static=arm64rt"); - } - // Linker arguments derived from WindowsDriver.KernelMode.props in Ni(22H2) WDK println!("cargo::rustc-cdylib-link-arg=/DRIVER"); println!("cargo::rustc-cdylib-link-arg=/NODEFAULTLIB"); @@ -1160,19 +1152,6 @@ impl Config { } DriverConfig::Kmdf(_) => { // Emit KMDF-specific libraries to link to - println!("cargo::rustc-link-lib=static=BufferOverflowFastFailK"); - println!("cargo::rustc-link-lib=static=ntoskrnl"); - println!("cargo::rustc-link-lib=static=hal"); - println!("cargo::rustc-link-lib=static=wmilib"); - println!("cargo::rustc-link-lib=static=WdfLdr"); - println!("cargo::rustc-link-lib=static=WdfDriverEntry"); - - // Emit ARM64-specific libraries to link to derived from - // WindowsDriver.arm64.props - if self.cpu_architecture == CpuArchitecture::Arm64 { - println!("cargo::rustc-link-lib=static=arm64rt"); - } - // Linker arguments derived from WindowsDriver.KernelMode.props in Ni(22H2) WDK println!("cargo::rustc-cdylib-link-arg=/DRIVER"); println!("cargo::rustc-cdylib-link-arg=/NODEFAULTLIB"); @@ -1187,16 +1166,9 @@ impl Config { // might not run` since `rustc` has no support for `/KERNEL` println!("cargo::rustc-cdylib-link-arg=/IGNORE:4257"); } - DriverConfig::Umdf(umdf_config) => { - // Emit UMDF-specific libraries to link to - if umdf_config.umdf_version_major >= 2 { - println!("cargo::rustc-link-lib=static=WdfDriverStubUm"); - println!("cargo::rustc-link-lib=static=ntdll"); - } - + DriverConfig::Umdf(_) => { println!("cargo::rustc-cdylib-link-arg=/NODEFAULTLIB:kernel32.lib"); println!("cargo::rustc-cdylib-link-arg=/NODEFAULTLIB:user32.lib"); - println!("cargo::rustc-link-lib=static=OneCoreUAP"); // Linker arguments derived from WindowsDriver.UserMode.props in Ni(22H2) WDK println!("cargo::rustc-cdylib-link-arg=/SUBSYSTEM:WINDOWS"); diff --git a/crates/wdk-sys/build.rs b/crates/wdk-sys/build.rs index 2a7308487..cccc63dbd 100644 --- a/crates/wdk-sys/build.rs +++ b/crates/wdk-sys/build.rs @@ -17,7 +17,7 @@ use std::{ }; use anyhow::Context; -use bindgen::CodegenConfig; +use bindgen::{Builder, CodegenConfig}; use tracing::{Span, info, info_span, trace}; use tracing_subscriber::{ EnvFilter, @@ -167,6 +167,129 @@ const BINDGEN_FILE_GENERATORS_TUPLES: &[(&str, GenerateFn)] = &[ ("usb.rs", generate_usb), ]; +/// A native library to link into the generated bindings. +/// +/// Rendered as a `#[link(...)]` attribute on an (empty) `extern` block and +/// emitted into the bindgen output via [`Builder::raw_line`]. An optional +/// `cfg` predicate gates the directive (e.g. by driver type). +#[allow( + dead_code, + reason = "reusable link-directive helper intended for use by multiple feature-gated bindgen \ + generators; unused when those features are disabled" +)] +#[derive(Debug, Clone)] +struct LinkDirective<'a> { + /// `#[link(name = "...")]` — the library to link. + name: &'a str, + /// `kind = "..."` — one of `static`, `dylib`, `raw-dylib`, `framework`. + /// `#[link(name = "VhfKm", kind = "...")]` + kind: &'a str, + /// `modifiers = "..."` values (e.g. `-bundle`, `+whole-archive`). Joined + /// with commas; an empty slice omits the `modifiers` key entirely. + /// `#[link(name = "VhfKm", kind = "static", modifiers = "...")]` + modifiers: &'a [&'a str], + /// ABI of the emitted `extern` block (e.g. `C`, `system`). + abi: &'a str, + /// Inner predicate of `#[cfg(...)]` gating the directive. `None` emits no + /// cfg. + cfg: Option<&'a str>, +} + +#[allow( + dead_code, + reason = "reusable link-directive helper intended for use by multiple feature-gated bindgen \ + generators; unused when those features are disabled" +)] +impl<'a> LinkDirective<'a> { + /// Creates a directive with the default `"C"` ABI, no modifiers, and no cfg + /// gate. + const fn new(name: &'a str, kind: &'a str) -> Self { + Self { + name, + kind, + modifiers: &[], + abi: "C", + cfg: None, + } + } + + const fn with_modifiers(mut self, modifiers: &'a [&'a str]) -> Self { + self.modifiers = modifiers; + self + } + + const fn with_abi(mut self, abi: &'a str) -> Self { + self.abi = abi; + self + } + + const fn with_cfg(mut self, cfg: &'a str) -> Self { + self.cfg = Some(cfg); + self + } + + /// Renders this directive into a self-contained block of Rust source. + fn render(&self) -> String { + const VALID_KINDS: [&str; 4] = ["static", "dylib", "raw-dylib", "framework"]; + + assert!( + !self.name.is_empty(), + "link directive `name` must not be empty" + ); + assert!( + !self.abi.is_empty(), + "link directive `abi` must not be empty" + ); + assert!( + VALID_KINDS.contains(&self.kind), + "unsupported link kind {:?}; expected one of {VALID_KINDS:?}", + self.kind + ); + assert!( + self.modifiers + .iter() + .all(|m| m.starts_with('+') || m.starts_with('-')), + "each link modifier must start with `+` or `-`: {:?}", + self.modifiers + ); + + let modifiers = if self.modifiers.is_empty() { + String::new() + } else { + format!(r#", modifiers = "{}""#, self.modifiers.join(",")) + }; + let link_attr = format!( + r#"#[link(name = "{}", kind = "{}"{modifiers})]"#, + self.name, self.kind + ); + let cfg_attr = self + .cfg + .map(|cfg| format!("#[cfg({cfg})]")) + .unwrap_or_default(); + + // Emitted as one block so the attributes always stay attached to the + // following `extern` block, regardless of where bindgen places raw + // lines. + format!( + "\n{cfg_attr}\n{link_attr}\nunsafe extern \"{}\" {{}}", + self.abi + ) + } +} + +/// Appends each [`LinkDirective`] to `builder` so rustc links the corresponding +/// libraries when compiling the generated bindings. +#[allow( + dead_code, + reason = "reusable link-directive helper intended for use by multiple feature-gated bindgen \ + generators; unused when those features are disabled" +)] +fn add_link_directives(builder: Builder, directives: &[LinkDirective]) -> Builder { + directives.iter().fold(builder, |builder, directive| { + builder.raw_line(directive.render()) + }) +} + fn initialize_tracing() -> Result<(), ParseError> { let tracing_filter = EnvFilter::default() // Show up to INFO level by default @@ -269,9 +392,13 @@ fn generate_base(out_path: &Path, config: &Config) -> Result<(), ConfigError> { let header_contents = config.bindgen_header_contents([ApiSubset::Base])?; trace!(header_contents = ?header_contents); - let bindgen_builder = bindgen::Builder::wdk_default(config)? - .with_codegen_config((CodegenConfig::TYPES | CodegenConfig::VARS).complement()) - .header_contents(&format!("{outfile_name}-input.h"), &header_contents); + let bindgen_builder = { + let builder = bindgen::Builder::wdk_default(config)? + .with_codegen_config((CodegenConfig::TYPES | CodegenConfig::VARS).complement()) + .header_contents(&format!("{outfile_name}-input.h"), &header_contents); + + add_link_directives(builder, &base_link_directives(config)) + }; trace!(bindgen_builder = ?bindgen_builder); let output_file_path = out_path.join(format!("{outfile_name}.rs")); @@ -282,6 +409,62 @@ fn generate_base(out_path: &Path, config: &Config) -> Result<(), ConfigError> { .map_err(|source| IoError::with_path(output_file_path, source))?) } +/// Builds the `static`-library link directives for the base bindings, mirroring +/// the `cargo::rustc-link-lib=static=*` directives that +/// [`wdk_build::Config::configure_binary_build`] emits for each driver model. +/// The `cargo::rustc-cdylib-link-arg=*` linker arguments it emits will +/// be moved over once . +/// +/// `-bundle` is used so the libraries are resolved during the final driver link +/// (where `configure_binary_build` emits the `rustc-link-search` paths) instead +/// of when `wdk-sys` itself is compiled. +/// +/// TODO: Once [link-arg-attribute](https://doc.rust-lang.org/unstable-book/language-features/link-arg-attribute.html) +/// stabilizes, the `cargo::rustc-cdylib-link-arg=*` directives from +/// [`wdk_build::Config::configure_binary_build`] will be added +fn base_link_directives(config: &Config) -> Vec> { + const fn static_lib(name: &'static str) -> LinkDirective<'static> { + LinkDirective::new(name, "static").with_modifiers(&["-bundle"]) + } + + let mut directives = Vec::new(); + match &config.driver_config { + // WDM-specific libraries to link to + DriverConfig::Wdm => { + directives.extend([ + static_lib("BufferOverflowFastFailK"), + static_lib("ntoskrnl"), + static_lib("hal"), + static_lib("wmilib"), + // ARM64-specific libraries to link to derived from + // WindowsDriver.arm64.props + static_lib("arm64rt").with_cfg(r#"target_arch = "aarch64""#), + ]); + } + DriverConfig::Kmdf(_) => { + // KMDF-specific libraries to link to + directives.extend([ + static_lib("BufferOverflowFastFailK"), + static_lib("ntoskrnl"), + static_lib("hal"), + static_lib("wmilib"), + static_lib("WdfLdr"), + static_lib("WdfDriverEntry"), + // ARM64-specific libraries to link to derived from + // WindowsDriver.arm64.props + static_lib("arm64rt").with_cfg(r#"target_arch = "aarch64""#), + ]); + } + DriverConfig::Umdf(umdf_config) => { + if umdf_config.umdf_version_major >= 2 { + directives.extend([static_lib("WdfDriverStubUm"), static_lib("ntdll")]); + } + directives.push(static_lib("OneCoreUAP")); + } + } + directives +} + fn generate_wdf(out_path: &Path, config: &Config) -> Result<(), ConfigError> { if let DriverConfig::Kmdf(_) | DriverConfig::Umdf(_) = config.driver_config { info!("Generating bindings to WDK: wdf.rs"); @@ -360,7 +543,19 @@ fn generate_hid(out_path: &Path, config: &Config) -> Result<(), ConfigError> { for header_file in config.headers(ApiSubset::Hid)? { builder = builder.allowlist_file(format!("(?i).*{header_file}.*")); } - builder + + // Add a link directive for VhfKm/VhfUm when feature "hid" enabled + // cfg's provide conditional compilation logic + let link_directives = [ + LinkDirective::new("VhfKm", "static") + .with_modifiers(&["-bundle"]) + .with_cfg( + r#"any(driver_model__driver_type = "KMDF", driver_model__driver_type = "WDM")"#, + ), + LinkDirective::new("VhfUm", "dylib").with_cfg(r#"driver_model__driver_type = "UMDF""#), + ]; + + add_link_directives(builder, &link_directives) }; trace!(bindgen_builder = ?bindgen_builder); From 0cce9660fe8ca7e8341bf1640a6c8337a7c7cf35 Mon Sep 17 00:00:00 2001 From: Alan Ngo Date: Thu, 18 Jun 2026 15:58:34 -0700 Subject: [PATCH 02/10] moved mechanism and data logic into wdk-build, wdk-sys now only selects by utilizing ApiSubset similar to the headers --- crates/wdk-build/src/lib.rs | 211 +++++++++++++++++++++++++++++++++++- crates/wdk-sys/build.rs | 197 +-------------------------------- 2 files changed, 214 insertions(+), 194 deletions(-) diff --git a/crates/wdk-build/src/lib.rs b/crates/wdk-build/src/lib.rs index a503cb7ac..571f0555f 100644 --- a/crates/wdk-build/src/lib.rs +++ b/crates/wdk-build/src/lib.rs @@ -18,6 +18,7 @@ use std::{ sync::LazyLock, }; +use ::bindgen::Builder; pub use bindgen::BuilderExt; use metadata::TryFromCargoMetadataError; use tracing::debug; @@ -1057,6 +1058,97 @@ impl Config { .collect()) } + /// Returns the native libraries that must be linked for a given + /// [`ApiSubset`], based off this [`Config`]. + /// + /// This is the link-directive analogue of [`Config::headers`]: it owns the + /// mapping from [`ApiSubset`] (and the active [`DriverConfig`] and + /// [`CpuArchitecture`]) to the set of [`LinkDirective`]s that should be + /// emitted into the generated bindings for that subset. Subsets that + /// contribute no extra libraries return an empty [`Vec`]. + /// + /// Unlike header selection, link subsets must be selected + /// *non-cumulatively*: each generated bindings file should request only + /// its own subset's libraries (e.g. `hid.rs` requests only + /// [`ApiSubset::Hid`], not [`ApiSubset::Base`]), since the base + /// libraries are already emitted into the base bindings file. This + /// avoids duplicate `#[link]` directives across generated files. + #[must_use] + pub fn link_directives(&self, api_subset: ApiSubset) -> Vec> { + match api_subset { + ApiSubset::Base => self.base_link_directives(), + ApiSubset::Hid => self.hid_link_directives(), + ApiSubset::Wdf + | ApiSubset::Gpio + | ApiSubset::ParallelPorts + | ApiSubset::Spb + | ApiSubset::Storage + | ApiSubset::Usb => Vec::new(), + } + } + + /// Builds the static library link directives for the base bindings. + /// + /// TODO: Once [link-arg-attribute](https://doc.rust-lang.org/unstable-book/language-features/link-arg-attribute.html) + /// stabilizes, the `cargo::rustc-cdylib-link-arg=*` directives from + /// [`wdk_build::Config::configure_binary_build`] will be added + #[tracing::instrument(level = "trace")] + fn base_link_directives(&self) -> Vec> { + const fn static_lib(name: &'static str) -> LinkDirective<'static> { + LinkDirective::new(name, "static").with_modifiers(&["-bundle"]) + } + + let mut directives = Vec::new(); + match &self.driver_config { + DriverConfig::Wdm => { + directives.extend([ + static_lib("BufferOverflowFastFailK"), + static_lib("ntoskrnl"), + static_lib("hal"), + static_lib("wmilib"), + ]); + } + DriverConfig::Kmdf(_) => { + directives.extend([ + static_lib("BufferOverflowFastFailK"), + static_lib("ntoskrnl"), + static_lib("hal"), + static_lib("wmilib"), + static_lib("WdfLdr"), + static_lib("WdfDriverEntry"), + ]); + } + DriverConfig::Umdf(umdf_config) => { + if umdf_config.umdf_version_major >= 2 { + directives.extend([static_lib("WdfDriverStubUm"), static_lib("ntdll")]); + } + directives.push(static_lib("OneCoreUAP")); + } + } + + // ARM64-specific libraries to link to derived from + // WindowsDriver.arm64.props. + if matches!( + self.driver_config, + DriverConfig::Wdm | DriverConfig::Kmdf(_) + ) && self.cpu_architecture == CpuArchitecture::Arm64 + { + directives.push(static_lib("arm64rt")); + } + + directives + } + + #[tracing::instrument(level = "trace")] + fn hid_link_directives(&self) -> Vec> { + match self.driver_config { + DriverConfig::Wdm | DriverConfig::Kmdf(_) => { + vec![LinkDirective::new("VhfKm", "static").with_modifiers(&["-bundle"])] + } + DriverConfig::Umdf(_) => vec![LinkDirective::new("VhfUm", "dylib")], + } + } + /// Configure a Cargo build of a library that depends on the WDK. This /// emits specially formatted prints to Cargo based on this [`Config`]. /// @@ -1126,7 +1218,7 @@ impl Config { } // TODO: Once [link-arg-attribute](https://doc.rust-lang.org/unstable-book/language-features/link-arg-attribute.html) - // stabilizes, the `cargo::rustc-cdylib-link-arg=*` directives will be moved to + // stabilizes, the `cargo::rustc-cdylib-link-arg=*` directives will be moved to // the wdk-sys build script match &self.driver_config { DriverConfig::Wdm => { @@ -1467,6 +1559,123 @@ pub fn configure_wdk_binary_build() -> Result<(), ConfigError> { Config::from_env_auto()?.configure_binary_build() } +/// A native library to link into the generated bindings. +/// +/// Rendered as a `#[link(...)]` attribute on an (empty) `extern` block and +/// emitted into the bindgen output via [`Builder::raw_line`]. An optional +/// `cfg` predicate gates the directive (e.g. by driver type). +#[derive(Debug, Clone)] +pub struct LinkDirective<'a> { + /// `#[link(name = "...")]` — the library to link. + name: &'a str, + /// `kind = "..."` — one of `static`, `dylib`, `raw-dylib`, `framework`. + /// `#[link(name = "VhfKm", kind = "...")]` + kind: &'a str, + /// `modifiers = "..."` values (e.g. `-bundle`, `+whole-archive`). Joined + /// with commas; an empty slice omits the `modifiers` key entirely. + /// `#[link(name = "VhfKm", kind = "static", modifiers = "...")]` + modifiers: &'a [&'a str], + /// ABI of the emitted `extern` block (e.g. `C`, `system`). + abi: &'a str, + /// Inner predicate of `#[cfg(...)]` gating the directive. `None` emits no + /// cfg. + cfg: Option<&'a str>, +} + +impl<'a> LinkDirective<'a> { + /// Creates a directive with the default `"C"` ABI, no modifiers, and no cfg + /// gate. + #[must_use] + pub const fn new(name: &'a str, kind: &'a str) -> Self { + Self { + name, + kind, + modifiers: &[], + abi: "C", + cfg: None, + } + } + + /// Sets the linking modifiers (e.g. `["-bundle"]`). Each modifier must be + /// prefixed with `+` or `-`. + #[must_use] + pub const fn with_modifiers(mut self, modifiers: &'a [&'a str]) -> Self { + self.modifiers = modifiers; + self + } + + /// Sets the ABI of the emitted `extern` block (defaults to `"C"`). + #[must_use] + pub const fn with_abi(mut self, abi: &'a str) -> Self { + self.abi = abi; + self + } + + /// Gates the directive behind a `#[cfg(...)]` predicate. + #[must_use] + pub const fn with_cfg(mut self, cfg: &'a str) -> Self { + self.cfg = Some(cfg); + self + } + + /// Renders this directive into a self-contained block of Rust source. + fn render(&self) -> String { + const VALID_KINDS: [&str; 4] = ["static", "dylib", "raw-dylib", "framework"]; + + assert!( + !self.name.is_empty(), + "link directive `name` must not be empty" + ); + assert!( + !self.abi.is_empty(), + "link directive `abi` must not be empty" + ); + assert!( + VALID_KINDS.contains(&self.kind), + "unsupported link kind {:?}; expected one of {VALID_KINDS:?}", + self.kind + ); + assert!( + self.modifiers + .iter() + .all(|m| m.starts_with('+') || m.starts_with('-')), + "each link modifier must start with `+` or `-`: {:?}", + self.modifiers + ); + + let modifiers = if self.modifiers.is_empty() { + String::new() + } else { + format!(r#", modifiers = "{}""#, self.modifiers.join(",")) + }; + let link_attr = format!( + r#"#[link(name = "{}", kind = "{}"{modifiers})]"#, + self.name, self.kind + ); + let cfg_attr = self + .cfg + .map(|cfg| format!("#[cfg({cfg})]")) + .unwrap_or_default(); + + // Emitted as one block so the attributes always stay attached to the + // following `extern` block, regardless of where bindgen places raw + // lines. + format!( + "\n{cfg_attr}\n{link_attr}\nunsafe extern \"{}\" {{}}", + self.abi + ) + } +} + +/// Appends each [`LinkDirective`] to `builder` so rustc links the corresponding +/// libraries when compiling the generated bindings. +#[must_use] +pub fn add_link_directives(builder: Builder, directives: &[LinkDirective]) -> Builder { + directives.iter().fold(builder, |builder, directive| { + builder.raw_line(directive.render()) + }) +} + /// This currently only exports the driver type, but may export more metadata in /// the future. `EXPORTED_CFG_SETTINGS` is a mapping of cfg key to allowed cfg /// values diff --git a/crates/wdk-sys/build.rs b/crates/wdk-sys/build.rs index cccc63dbd..597ae79be 100644 --- a/crates/wdk-sys/build.rs +++ b/crates/wdk-sys/build.rs @@ -17,7 +17,7 @@ use std::{ }; use anyhow::Context; -use bindgen::{Builder, CodegenConfig}; +use bindgen::CodegenConfig; use tracing::{Span, info, info_span, trace}; use tracing_subscriber::{ EnvFilter, @@ -32,6 +32,7 @@ use wdk_build::{ IoError, KmdfConfig, UmdfConfig, + add_link_directives, configure_wdk_library_build_and_then, }; @@ -167,129 +168,6 @@ const BINDGEN_FILE_GENERATORS_TUPLES: &[(&str, GenerateFn)] = &[ ("usb.rs", generate_usb), ]; -/// A native library to link into the generated bindings. -/// -/// Rendered as a `#[link(...)]` attribute on an (empty) `extern` block and -/// emitted into the bindgen output via [`Builder::raw_line`]. An optional -/// `cfg` predicate gates the directive (e.g. by driver type). -#[allow( - dead_code, - reason = "reusable link-directive helper intended for use by multiple feature-gated bindgen \ - generators; unused when those features are disabled" -)] -#[derive(Debug, Clone)] -struct LinkDirective<'a> { - /// `#[link(name = "...")]` — the library to link. - name: &'a str, - /// `kind = "..."` — one of `static`, `dylib`, `raw-dylib`, `framework`. - /// `#[link(name = "VhfKm", kind = "...")]` - kind: &'a str, - /// `modifiers = "..."` values (e.g. `-bundle`, `+whole-archive`). Joined - /// with commas; an empty slice omits the `modifiers` key entirely. - /// `#[link(name = "VhfKm", kind = "static", modifiers = "...")]` - modifiers: &'a [&'a str], - /// ABI of the emitted `extern` block (e.g. `C`, `system`). - abi: &'a str, - /// Inner predicate of `#[cfg(...)]` gating the directive. `None` emits no - /// cfg. - cfg: Option<&'a str>, -} - -#[allow( - dead_code, - reason = "reusable link-directive helper intended for use by multiple feature-gated bindgen \ - generators; unused when those features are disabled" -)] -impl<'a> LinkDirective<'a> { - /// Creates a directive with the default `"C"` ABI, no modifiers, and no cfg - /// gate. - const fn new(name: &'a str, kind: &'a str) -> Self { - Self { - name, - kind, - modifiers: &[], - abi: "C", - cfg: None, - } - } - - const fn with_modifiers(mut self, modifiers: &'a [&'a str]) -> Self { - self.modifiers = modifiers; - self - } - - const fn with_abi(mut self, abi: &'a str) -> Self { - self.abi = abi; - self - } - - const fn with_cfg(mut self, cfg: &'a str) -> Self { - self.cfg = Some(cfg); - self - } - - /// Renders this directive into a self-contained block of Rust source. - fn render(&self) -> String { - const VALID_KINDS: [&str; 4] = ["static", "dylib", "raw-dylib", "framework"]; - - assert!( - !self.name.is_empty(), - "link directive `name` must not be empty" - ); - assert!( - !self.abi.is_empty(), - "link directive `abi` must not be empty" - ); - assert!( - VALID_KINDS.contains(&self.kind), - "unsupported link kind {:?}; expected one of {VALID_KINDS:?}", - self.kind - ); - assert!( - self.modifiers - .iter() - .all(|m| m.starts_with('+') || m.starts_with('-')), - "each link modifier must start with `+` or `-`: {:?}", - self.modifiers - ); - - let modifiers = if self.modifiers.is_empty() { - String::new() - } else { - format!(r#", modifiers = "{}""#, self.modifiers.join(",")) - }; - let link_attr = format!( - r#"#[link(name = "{}", kind = "{}"{modifiers})]"#, - self.name, self.kind - ); - let cfg_attr = self - .cfg - .map(|cfg| format!("#[cfg({cfg})]")) - .unwrap_or_default(); - - // Emitted as one block so the attributes always stay attached to the - // following `extern` block, regardless of where bindgen places raw - // lines. - format!( - "\n{cfg_attr}\n{link_attr}\nunsafe extern \"{}\" {{}}", - self.abi - ) - } -} - -/// Appends each [`LinkDirective`] to `builder` so rustc links the corresponding -/// libraries when compiling the generated bindings. -#[allow( - dead_code, - reason = "reusable link-directive helper intended for use by multiple feature-gated bindgen \ - generators; unused when those features are disabled" -)] -fn add_link_directives(builder: Builder, directives: &[LinkDirective]) -> Builder { - directives.iter().fold(builder, |builder, directive| { - builder.raw_line(directive.render()) - }) -} - fn initialize_tracing() -> Result<(), ParseError> { let tracing_filter = EnvFilter::default() // Show up to INFO level by default @@ -397,7 +275,7 @@ fn generate_base(out_path: &Path, config: &Config) -> Result<(), ConfigError> { .with_codegen_config((CodegenConfig::TYPES | CodegenConfig::VARS).complement()) .header_contents(&format!("{outfile_name}-input.h"), &header_contents); - add_link_directives(builder, &base_link_directives(config)) + add_link_directives(builder, &config.link_directives(ApiSubset::Base)) }; trace!(bindgen_builder = ?bindgen_builder); @@ -409,62 +287,6 @@ fn generate_base(out_path: &Path, config: &Config) -> Result<(), ConfigError> { .map_err(|source| IoError::with_path(output_file_path, source))?) } -/// Builds the `static`-library link directives for the base bindings, mirroring -/// the `cargo::rustc-link-lib=static=*` directives that -/// [`wdk_build::Config::configure_binary_build`] emits for each driver model. -/// The `cargo::rustc-cdylib-link-arg=*` linker arguments it emits will -/// be moved over once . -/// -/// `-bundle` is used so the libraries are resolved during the final driver link -/// (where `configure_binary_build` emits the `rustc-link-search` paths) instead -/// of when `wdk-sys` itself is compiled. -/// -/// TODO: Once [link-arg-attribute](https://doc.rust-lang.org/unstable-book/language-features/link-arg-attribute.html) -/// stabilizes, the `cargo::rustc-cdylib-link-arg=*` directives from -/// [`wdk_build::Config::configure_binary_build`] will be added -fn base_link_directives(config: &Config) -> Vec> { - const fn static_lib(name: &'static str) -> LinkDirective<'static> { - LinkDirective::new(name, "static").with_modifiers(&["-bundle"]) - } - - let mut directives = Vec::new(); - match &config.driver_config { - // WDM-specific libraries to link to - DriverConfig::Wdm => { - directives.extend([ - static_lib("BufferOverflowFastFailK"), - static_lib("ntoskrnl"), - static_lib("hal"), - static_lib("wmilib"), - // ARM64-specific libraries to link to derived from - // WindowsDriver.arm64.props - static_lib("arm64rt").with_cfg(r#"target_arch = "aarch64""#), - ]); - } - DriverConfig::Kmdf(_) => { - // KMDF-specific libraries to link to - directives.extend([ - static_lib("BufferOverflowFastFailK"), - static_lib("ntoskrnl"), - static_lib("hal"), - static_lib("wmilib"), - static_lib("WdfLdr"), - static_lib("WdfDriverEntry"), - // ARM64-specific libraries to link to derived from - // WindowsDriver.arm64.props - static_lib("arm64rt").with_cfg(r#"target_arch = "aarch64""#), - ]); - } - DriverConfig::Umdf(umdf_config) => { - if umdf_config.umdf_version_major >= 2 { - directives.extend([static_lib("WdfDriverStubUm"), static_lib("ntdll")]); - } - directives.push(static_lib("OneCoreUAP")); - } - } - directives -} - fn generate_wdf(out_path: &Path, config: &Config) -> Result<(), ConfigError> { if let DriverConfig::Kmdf(_) | DriverConfig::Umdf(_) = config.driver_config { info!("Generating bindings to WDK: wdf.rs"); @@ -544,18 +366,7 @@ fn generate_hid(out_path: &Path, config: &Config) -> Result<(), ConfigError> { builder = builder.allowlist_file(format!("(?i).*{header_file}.*")); } - // Add a link directive for VhfKm/VhfUm when feature "hid" enabled - // cfg's provide conditional compilation logic - let link_directives = [ - LinkDirective::new("VhfKm", "static") - .with_modifiers(&["-bundle"]) - .with_cfg( - r#"any(driver_model__driver_type = "KMDF", driver_model__driver_type = "WDM")"#, - ), - LinkDirective::new("VhfUm", "dylib").with_cfg(r#"driver_model__driver_type = "UMDF""#), - ]; - - add_link_directives(builder, &link_directives) + add_link_directives(builder, &config.link_directives(ApiSubset::Hid)) }; trace!(bindgen_builder = ?bindgen_builder); From a3b51b8b53ecf3043bce8d36ca8afce9f29f3310 Mon Sep 17 00:00:00 2001 From: Alan Ngo Date: Fri, 19 Jun 2026 11:29:22 -0700 Subject: [PATCH 03/10] utilize the test-stubs feature in wdk-sys to gate the new link attributes out when building non-driver test wdk-sys-tests (wdk-sys-tests does not call configure_binary_build thus no search paths are emitted and ungated link attributes fail the build/test) --- crates/wdk-build/src/lib.rs | 28 +++++++++++++++++++++++++--- tests/wdk-sys-tests/Cargo.toml | 2 +- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/crates/wdk-build/src/lib.rs b/crates/wdk-build/src/lib.rs index 571f0555f..980d9adfb 100644 --- a/crates/wdk-build/src/lib.rs +++ b/crates/wdk-build/src/lib.rs @@ -1095,7 +1095,12 @@ impl Config { #[tracing::instrument(level = "trace")] fn base_link_directives(&self) -> Vec> { const fn static_lib(name: &'static str) -> LinkDirective<'static> { - LinkDirective::new(name, "static").with_modifiers(&["-bundle"]) + // Gated on `NOT_TEST_STUBS_CFG` so non-driver test consumers don't + // link the real WDK libraries (see the const's docs for the full + // rationale). + LinkDirective::new(name, "static") + .with_modifiers(&["-bundle"]) + .with_cfg(NOT_TEST_STUBS_CFG) } let mut directives = Vec::new(); @@ -1143,9 +1148,15 @@ impl Config { fn hid_link_directives(&self) -> Vec> { match self.driver_config { DriverConfig::Wdm | DriverConfig::Kmdf(_) => { - vec![LinkDirective::new("VhfKm", "static").with_modifiers(&["-bundle"])] + vec![ + LinkDirective::new("VhfKm", "static") + .with_modifiers(&["-bundle"]) + .with_cfg(NOT_TEST_STUBS_CFG), + ] + } + DriverConfig::Umdf(_) => { + vec![LinkDirective::new("VhfUm", "dylib").with_cfg(NOT_TEST_STUBS_CFG)] } - DriverConfig::Umdf(_) => vec![LinkDirective::new("VhfUm", "dylib")], } } @@ -1559,6 +1570,17 @@ pub fn configure_wdk_binary_build() -> Result<(), ConfigError> { Config::from_env_auto()?.configure_binary_build() } +/// `cfg` predicate that gates the WDK link directives so that they are only +/// emitted when the `wdk-sys` `test-stubs` feature is *not* enabled. +/// +/// Downstream test executables that depend on `wdk-sys` enable its `test-stubs` +/// feature (which provides stubbed symbols *and*, via this gate, suppresses +/// real WDK library linkage; see `wdk_sys::test_stubs`). Such executables are +/// not driver binaries, so they never receive the WDK `rustc-link-search` paths +/// that `Config::configure_binary_build` emits, and must therefore not attempt +/// to link the real WDK libraries. +const NOT_TEST_STUBS_CFG: &str = r#"not(feature = "test-stubs")"#; + /// A native library to link into the generated bindings. /// /// Rendered as a `#[link(...)]` attribute on an (empty) `extern` block and diff --git a/tests/wdk-sys-tests/Cargo.toml b/tests/wdk-sys-tests/Cargo.toml index 1c67faaca..59bd24276 100644 --- a/tests/wdk-sys-tests/Cargo.toml +++ b/tests/wdk-sys-tests/Cargo.toml @@ -12,4 +12,4 @@ driver-type = "WDM" [lib] [dependencies] -wdk-sys = { path = "../../crates/wdk-sys" } +wdk-sys = { features = ["test-stubs"], path = "../../crates/wdk-sys" } From 453f9db3de62dcd9bcf7452ec3f289f3ae3cc101 Mon Sep 17 00:00:00 2001 From: Alan Ngo Date: Fri, 19 Jun 2026 16:42:00 -0700 Subject: [PATCH 04/10] -redesign to closely match Config::headers pattern (apisubset selection, library selection, and link directive string creation) -removed raw strings in lieu of types to tightly constrain LinkDirective creation w/o needing assert checks -removed and compacted unneeded functions from original implementation --- crates/wdk-build/src/lib.rs | 259 +++++++++++++++++++----------------- crates/wdk-sys/build.rs | 14 +- 2 files changed, 139 insertions(+), 134 deletions(-) diff --git a/crates/wdk-build/src/lib.rs b/crates/wdk-build/src/lib.rs index 980d9adfb..7f80810fc 100644 --- a/crates/wdk-build/src/lib.rs +++ b/crates/wdk-build/src/lib.rs @@ -18,7 +18,6 @@ use std::{ sync::LazyLock, }; -use ::bindgen::Builder; pub use bindgen::BuilderExt; use metadata::TryFromCargoMetadataError; use tracing::debug; @@ -1058,26 +1057,45 @@ impl Config { .collect()) } - /// Returns the native libraries that must be linked for a given - /// [`ApiSubset`], based off this [`Config`]. + /// Returns the formatted `#[link]` raw lines for the given [`ApiSubset`], + /// ready to be passed to bindgen's `raw_line`. + /// + /// This is the link-directive analogue of + /// [`Config::bindgen_header_contents`]: it maps the [`ApiSubset`] (and the + /// active [`DriverConfig`] and [`CpuArchitecture`]) to the native libraries + /// that must be linked, rendering them as `#[link(...)]` attributes emitted + /// into the generated bindings. /// - /// This is the link-directive analogue of [`Config::headers`]: it owns the - /// mapping from [`ApiSubset`] (and the active [`DriverConfig`] and - /// [`CpuArchitecture`]) to the set of [`LinkDirective`]s that should be - /// emitted into the generated bindings for that subset. Subsets that - /// contribute no extra libraries return an empty [`Vec`]. + /// Unlike header selection, link subsets are selected *non-cumulatively*: + /// each generated bindings file requests only its own subset's libraries + /// (e.g. `hid.rs` requests only [`ApiSubset::Hid`], not + /// [`ApiSubset::Base`]), since the base libraries are already emitted + /// into the base bindings file. This avoids duplicate `#[link]` + /// directives across generated files, and is why this takes a single + /// [`ApiSubset`] rather than a set like + /// [`Config::bindgen_header_contents`]. /// - /// Unlike header selection, link subsets must be selected - /// *non-cumulatively*: each generated bindings file should request only - /// its own subset's libraries (e.g. `hid.rs` requests only - /// [`ApiSubset::Hid`], not [`ApiSubset::Base`]), since the base - /// libraries are already emitted into the base bindings file. This - /// avoids duplicate `#[link]` directives across generated files. + /// Each emitted directive is gated behind `#[cfg(not(feature = + /// "test-stubs"))]`, evaluated in the crate that compiles the generated + /// bindings (`wdk-sys`), so the directives are suppressed when `wdk-sys` is + /// built with its `test-stubs` feature. #[must_use] - pub fn link_directives(&self, api_subset: ApiSubset) -> Vec> { + pub fn bindgen_library_link_raw_lines(&self, api_subset: ApiSubset) -> String { + self.libraries(api_subset) + .iter() + .map(LinkDirective::render) + .collect() + } + + /// Returns the native libraries that must be linked for a given + /// [`ApiSubset`], based off this [`Config`]. Subsets that contribute no + /// extra libraries return an empty [`Vec`]. + /// + /// This is the link-directive analogue of [`Config::headers`]. + fn libraries(&self, api_subset: ApiSubset) -> Vec { match api_subset { - ApiSubset::Base => self.base_link_directives(), - ApiSubset::Hid => self.hid_link_directives(), + ApiSubset::Base => self.base_libraries(), + ApiSubset::Hid => self.hid_libraries(), ApiSubset::Wdf | ApiSubset::Gpio | ApiSubset::ParallelPorts @@ -1090,20 +1108,24 @@ impl Config { /// Builds the static library link directives for the base bindings. /// /// TODO: Once [link-arg-attribute](https://doc.rust-lang.org/unstable-book/language-features/link-arg-attribute.html) - /// stabilizes, the `cargo::rustc-cdylib-link-arg=*` directives from - /// [`wdk_build::Config::configure_binary_build`] will be added + /// stabilizes, the `cargo::rustc-cdylib-link-arg=*` directives emitted by + /// [`wdk_build::Config::configure_binary_build`] will be moved here too. #[tracing::instrument(level = "trace")] - fn base_link_directives(&self) -> Vec> { - const fn static_lib(name: &'static str) -> LinkDirective<'static> { - // Gated on `NOT_TEST_STUBS_CFG` so non-driver test consumers don't - // link the real WDK libraries (see the const's docs for the full - // rationale). - LinkDirective::new(name, "static") - .with_modifiers(&["-bundle"]) - .with_cfg(NOT_TEST_STUBS_CFG) + fn base_libraries(&self) -> Vec { + const fn static_lib(name: &'static str) -> LinkDirective { + // `LinkModifier::NoBundle` (rustc `-bundle`): don't pack the static + // lib into wdk-sys's rlib; defer its linking to the final driver + // binary, which is where the WDK `rustc-link-search` paths are + // emitted (by `Config::configure_binary_build`). Without it, building + // wdk-sys's rlib would fail since those search paths aren't available + // then. + LinkDirective::new(name, LinkKind::Static).with_modifiers(&[LinkModifier::NoBundle]) } let mut directives = Vec::new(); + + // Base libraries derived from WindowsDriver.KernelMode.props (WDM/KMDF) + // and WindowsDriver.UserMode.props (UMDF) in the Ni(22H2) WDK. match &self.driver_config { DriverConfig::Wdm => { directives.extend([ @@ -1145,18 +1167,17 @@ impl Config { } #[tracing::instrument(level = "trace")] - fn hid_link_directives(&self) -> Vec> { + fn hid_libraries(&self) -> Vec { match self.driver_config { DriverConfig::Wdm | DriverConfig::Kmdf(_) => { + // `LinkModifier::NoBundle` for the same reason as the base + // static libs; see `static_lib` in `base_libraries`. vec![ - LinkDirective::new("VhfKm", "static") - .with_modifiers(&["-bundle"]) - .with_cfg(NOT_TEST_STUBS_CFG), + LinkDirective::new("VhfKm", LinkKind::Static) + .with_modifiers(&[LinkModifier::NoBundle]), ] } - DriverConfig::Umdf(_) => { - vec![LinkDirective::new("VhfUm", "dylib").with_cfg(NOT_TEST_STUBS_CFG)] - } + DriverConfig::Umdf(_) => vec![LinkDirective::new("VhfUm", LinkKind::Dylib)], } } @@ -1233,7 +1254,7 @@ impl Config { // the wdk-sys build script match &self.driver_config { DriverConfig::Wdm => { - // Emit WDM-specific libraries to link to + // Emit WDM-specific linker args // Linker arguments derived from WindowsDriver.KernelMode.props in Ni(22H2) WDK println!("cargo::rustc-cdylib-link-arg=/DRIVER"); println!("cargo::rustc-cdylib-link-arg=/NODEFAULTLIB"); @@ -1254,7 +1275,7 @@ impl Config { println!("cargo::rustc-cdylib-link-arg=/IGNORE:4216"); } DriverConfig::Kmdf(_) => { - // Emit KMDF-specific libraries to link to + // Emit KMDF-specific linker args // Linker arguments derived from WindowsDriver.KernelMode.props in Ni(22H2) WDK println!("cargo::rustc-cdylib-link-arg=/DRIVER"); println!("cargo::rustc-cdylib-link-arg=/NODEFAULTLIB"); @@ -1581,123 +1602,111 @@ pub fn configure_wdk_binary_build() -> Result<(), ConfigError> { /// to link the real WDK libraries. const NOT_TEST_STUBS_CFG: &str = r#"not(feature = "test-stubs")"#; +/// The `kind` of a `#[link]` attribute (i.e. how the library is linked). +/// +/// Only the kinds actually emitted by [`Config`] are modelled; see the +/// [rustc reference][1] for the full set. +/// +/// [1]: https://doc.rust-lang.org/reference/items/external-blocks.html#the-link-attribute +#[derive(Debug, Clone, Copy)] +enum LinkKind { + /// `kind = "static"` + Static, + /// `kind = "dylib"` + Dylib, +} + +impl LinkKind { + /// Returns the string used in the `kind = "..."` field of a `#[link]` + /// attribute. + const fn as_str(self) -> &'static str { + match self { + Self::Static => "static", + Self::Dylib => "dylib", + } + } +} + +/// A linking modifier for a `#[link]` attribute (the `modifiers = "..."` +/// field). Only the modifiers actually emitted by [`Config`] are modelled. +#[derive(Debug, Clone, Copy)] +enum LinkModifier { + /// `-bundle` + NoBundle, +} + +impl LinkModifier { + /// Returns the string used in the `modifiers = "..."` field of a `#[link]` + /// attribute, including its `+`/`-` prefix. + const fn as_str(self) -> &'static str { + match self { + Self::NoBundle => "-bundle", + } + } +} + /// A native library to link into the generated bindings. /// -/// Rendered as a `#[link(...)]` attribute on an (empty) `extern` block and -/// emitted into the bindgen output via [`Builder::raw_line`]. An optional -/// `cfg` predicate gates the directive (e.g. by driver type). +/// Rendered by [`LinkDirective::render`] as a `#[link(...)]` attribute on an +/// (empty) `extern` block and emitted into the bindgen output via bindgen's +/// `raw_line`. Every directive is gated behind `NOT_TEST_STUBS_CFG` so that +/// non-driver test consumers don't link the real WDK libraries. #[derive(Debug, Clone)] -pub struct LinkDirective<'a> { - /// `#[link(name = "...")]` — the library to link. - name: &'a str, - /// `kind = "..."` — one of `static`, `dylib`, `raw-dylib`, `framework`. - /// `#[link(name = "VhfKm", kind = "...")]` - kind: &'a str, - /// `modifiers = "..."` values (e.g. `-bundle`, `+whole-archive`). Joined +struct LinkDirective { + /// `name = "..."` — the library to link. + name: &'static str, + /// `kind = "..."` — how the library is linked. + kind: LinkKind, + /// `modifiers = "..."` values (e.g. [`LinkModifier::NoBundle`]). Joined /// with commas; an empty slice omits the `modifiers` key entirely. - /// `#[link(name = "VhfKm", kind = "static", modifiers = "...")]` - modifiers: &'a [&'a str], - /// ABI of the emitted `extern` block (e.g. `C`, `system`). - abi: &'a str, - /// Inner predicate of `#[cfg(...)]` gating the directive. `None` emits no - /// cfg. - cfg: Option<&'a str>, + modifiers: &'static [LinkModifier], } -impl<'a> LinkDirective<'a> { - /// Creates a directive with the default `"C"` ABI, no modifiers, and no cfg - /// gate. - #[must_use] - pub const fn new(name: &'a str, kind: &'a str) -> Self { +impl LinkDirective { + /// Creates a directive with no modifiers. + const fn new(name: &'static str, kind: LinkKind) -> Self { Self { name, kind, modifiers: &[], - abi: "C", - cfg: None, } } - /// Sets the linking modifiers (e.g. `["-bundle"]`). Each modifier must be - /// prefixed with `+` or `-`. - #[must_use] - pub const fn with_modifiers(mut self, modifiers: &'a [&'a str]) -> Self { + /// Sets the linking modifiers (e.g. `[LinkModifier::NoBundle]`). + const fn with_modifiers(mut self, modifiers: &'static [LinkModifier]) -> Self { self.modifiers = modifiers; self } - /// Sets the ABI of the emitted `extern` block (defaults to `"C"`). - #[must_use] - pub const fn with_abi(mut self, abi: &'a str) -> Self { - self.abi = abi; - self - } - - /// Gates the directive behind a `#[cfg(...)]` predicate. - #[must_use] - pub const fn with_cfg(mut self, cfg: &'a str) -> Self { - self.cfg = Some(cfg); - self - } - /// Renders this directive into a self-contained block of Rust source. + /// + /// Emitted as a single block so the attributes always stay attached to the + /// following `extern` block, regardless of where bindgen places raw lines. fn render(&self) -> String { - const VALID_KINDS: [&str; 4] = ["static", "dylib", "raw-dylib", "framework"]; - - assert!( - !self.name.is_empty(), - "link directive `name` must not be empty" - ); - assert!( - !self.abi.is_empty(), - "link directive `abi` must not be empty" - ); - assert!( - VALID_KINDS.contains(&self.kind), - "unsupported link kind {:?}; expected one of {VALID_KINDS:?}", - self.kind - ); - assert!( - self.modifiers - .iter() - .all(|m| m.starts_with('+') || m.starts_with('-')), - "each link modifier must start with `+` or `-`: {:?}", - self.modifiers - ); - let modifiers = if self.modifiers.is_empty() { String::new() } else { - format!(r#", modifiers = "{}""#, self.modifiers.join(",")) + let modifiers = self + .modifiers + .iter() + .map(|modifier| modifier.as_str()) + .collect::>() + .join(","); + format!(r#", modifiers = "{modifiers}""#) }; - let link_attr = format!( - r#"#[link(name = "{}", kind = "{}"{modifiers})]"#, - self.name, self.kind - ); - let cfg_attr = self - .cfg - .map(|cfg| format!("#[cfg({cfg})]")) - .unwrap_or_default(); - - // Emitted as one block so the attributes always stay attached to the - // following `extern` block, regardless of where bindgen places raw - // lines. + format!( - "\n{cfg_attr}\n{link_attr}\nunsafe extern \"{}\" {{}}", - self.abi + r#" +#[cfg({cfg})] +#[link(name = "{name}", kind = "{kind}"{modifiers})] +unsafe extern "C" {{}}"#, + cfg = NOT_TEST_STUBS_CFG, + name = self.name, + kind = self.kind.as_str(), ) } } -/// Appends each [`LinkDirective`] to `builder` so rustc links the corresponding -/// libraries when compiling the generated bindings. -#[must_use] -pub fn add_link_directives(builder: Builder, directives: &[LinkDirective]) -> Builder { - directives.iter().fold(builder, |builder, directive| { - builder.raw_line(directive.render()) - }) -} - /// This currently only exports the driver type, but may export more metadata in /// the future. `EXPORTED_CFG_SETTINGS` is a mapping of cfg key to allowed cfg /// values diff --git a/crates/wdk-sys/build.rs b/crates/wdk-sys/build.rs index 597ae79be..7ccaa52fc 100644 --- a/crates/wdk-sys/build.rs +++ b/crates/wdk-sys/build.rs @@ -32,7 +32,6 @@ use wdk_build::{ IoError, KmdfConfig, UmdfConfig, - add_link_directives, configure_wdk_library_build_and_then, }; @@ -270,13 +269,10 @@ fn generate_base(out_path: &Path, config: &Config) -> Result<(), ConfigError> { let header_contents = config.bindgen_header_contents([ApiSubset::Base])?; trace!(header_contents = ?header_contents); - let bindgen_builder = { - let builder = bindgen::Builder::wdk_default(config)? - .with_codegen_config((CodegenConfig::TYPES | CodegenConfig::VARS).complement()) - .header_contents(&format!("{outfile_name}-input.h"), &header_contents); - - add_link_directives(builder, &config.link_directives(ApiSubset::Base)) - }; + let bindgen_builder = bindgen::Builder::wdk_default(config)? + .with_codegen_config((CodegenConfig::TYPES | CodegenConfig::VARS).complement()) + .header_contents(&format!("{outfile_name}-input.h"), &header_contents) + .raw_line(config.bindgen_library_link_raw_lines(ApiSubset::Base)); trace!(bindgen_builder = ?bindgen_builder); let output_file_path = out_path.join(format!("{outfile_name}.rs")); @@ -366,7 +362,7 @@ fn generate_hid(out_path: &Path, config: &Config) -> Result<(), ConfigError> { builder = builder.allowlist_file(format!("(?i).*{header_file}.*")); } - add_link_directives(builder, &config.link_directives(ApiSubset::Hid)) + builder.raw_line(config.bindgen_library_link_raw_lines(ApiSubset::Hid)) }; trace!(bindgen_builder = ?bindgen_builder); From f8f26b723b0bfaacc2f7374931630379d058c7bf Mon Sep 17 00:00:00 2001 From: Alan Ngo Date: Mon, 22 Jun 2026 15:10:01 -0700 Subject: [PATCH 05/10] - add a cfg() conditional compilation guard on not(test)) to the link attributes to ensure wdk-sys builds correctly when compiled for test - add unit tests - add PartialEq and Eq traits to LinkKind, LinkModifier, and LinkDirective --- crates/wdk-build/src/lib.rs | 294 ++++++++++++++++++++++++++++++++++-- 1 file changed, 283 insertions(+), 11 deletions(-) diff --git a/crates/wdk-build/src/lib.rs b/crates/wdk-build/src/lib.rs index 7f80810fc..b39760784 100644 --- a/crates/wdk-build/src/lib.rs +++ b/crates/wdk-build/src/lib.rs @@ -1075,10 +1075,13 @@ impl Config { /// [`ApiSubset`] rather than a set like /// [`Config::bindgen_header_contents`]. /// - /// Each emitted directive is gated behind `#[cfg(not(feature = - /// "test-stubs"))]`, evaluated in the crate that compiles the generated - /// bindings (`wdk-sys`), so the directives are suppressed when `wdk-sys` is - /// built with its `test-stubs` feature. + /// Each emitted directive is gated behind + /// `#[cfg(not(any(test, feature = "test-stubs")))]`, evaluated in the crate + /// that compiles the generated bindings (`wdk-sys`). The directives are + /// therefore suppressed when `wdk-sys` is built with its `test-stubs` + /// feature, or when `wdk-sys` is itself compiled as a test target (e.g. + /// `cargo test -p wdk-sys`); see the `NOT_TEST_CFG` constant for the + /// rationale. #[must_use] pub fn bindgen_library_link_raw_lines(&self, api_subset: ApiSubset) -> String { self.libraries(api_subset) @@ -1592,7 +1595,7 @@ pub fn configure_wdk_binary_build() -> Result<(), ConfigError> { } /// `cfg` predicate that gates the WDK link directives so that they are only -/// emitted when the `wdk-sys` `test-stubs` feature is *not* enabled. +/// emitted when the bindings are compiled for a real driver build. /// /// Downstream test executables that depend on `wdk-sys` enable its `test-stubs` /// feature (which provides stubbed symbols *and*, via this gate, suppresses @@ -1600,7 +1603,13 @@ pub fn configure_wdk_binary_build() -> Result<(), ConfigError> { /// not driver binaries, so they never receive the WDK `rustc-link-search` paths /// that `Config::configure_binary_build` emits, and must therefore not attempt /// to link the real WDK libraries. -const NOT_TEST_STUBS_CFG: &str = r#"not(feature = "test-stubs")"#; +/// +/// The `test` predicate covers the analogous case where `wdk-sys` *itself* is +/// compiled as a test target (e.g. `cargo test -p wdk-sys`): that build goes +/// through `configure_library_build` rather than `configure_binary_build`, so +/// it likewise lacks the `rustc-link-search` paths and must not link the real +/// WDK libraries. +const NOT_TEST_CFG: &str = r#"not(any(test, feature = "test-stubs"))"#; /// The `kind` of a `#[link]` attribute (i.e. how the library is linked). /// @@ -1608,7 +1617,7 @@ const NOT_TEST_STUBS_CFG: &str = r#"not(feature = "test-stubs")"#; /// [rustc reference][1] for the full set. /// /// [1]: https://doc.rust-lang.org/reference/items/external-blocks.html#the-link-attribute -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] enum LinkKind { /// `kind = "static"` Static, @@ -1629,7 +1638,7 @@ impl LinkKind { /// A linking modifier for a `#[link]` attribute (the `modifiers = "..."` /// field). Only the modifiers actually emitted by [`Config`] are modelled. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] enum LinkModifier { /// `-bundle` NoBundle, @@ -1649,9 +1658,9 @@ impl LinkModifier { /// /// Rendered by [`LinkDirective::render`] as a `#[link(...)]` attribute on an /// (empty) `extern` block and emitted into the bindgen output via bindgen's -/// `raw_line`. Every directive is gated behind `NOT_TEST_STUBS_CFG` so that +/// `raw_line`. Every directive is gated behind [`NOT_TEST_CFG`] so that /// non-driver test consumers don't link the real WDK libraries. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] struct LinkDirective { /// `name = "..."` — the library to link. name: &'static str, @@ -1700,7 +1709,7 @@ impl LinkDirective { #[cfg({cfg})] #[link(name = "{name}", kind = "{kind}"{modifiers})] unsafe extern "C" {{}}"#, - cfg = NOT_TEST_STUBS_CFG, + cfg = NOT_TEST_CFG, name = self.name, kind = self.kind.as_str(), ) @@ -2262,6 +2271,269 @@ mod tests { } } + mod link_directive_contents { + use super::*; + use crate::{KmdfConfig, UmdfConfig}; + + /// Collects the linked library names from a slice of + /// [`LinkDirective`]s, in order. + fn lib_names(directives: &[LinkDirective]) -> Vec<&str> { + directives.iter().map(|directive| directive.name).collect() + } + + /// Builds a [`Config`] for the given target architecture and driver + /// model. + /// + /// Constructs [`Config`] directly (rather than via + /// `..Config::default()`) so these pure link-directive tests + /// don't require a real WDK/eWDK to be + /// present; `wdk_content_root` is irrelevant to the logic under test. + fn config_for(target_arch: &str, driver_config: DriverConfig) -> Config { + Config { + wdk_content_root: std::path::PathBuf::new(), + driver_config, + cpu_architecture: CpuArchitecture::try_from_cargo_str(target_arch) + .expect("test target_arch should be a recognized CpuArchitecture"), + } + } + + #[test] + fn render_minimal_directive() { + assert_eq!( + LinkDirective::new("Foo", LinkKind::Dylib).render(), + format!( + "\n#[cfg({NOT_TEST_CFG})]\n#[link(name = \"Foo\", kind = \"dylib\")]\nunsafe \ + extern \"C\" {{}}" + ) + ); + } + + #[test] + fn render_static_without_modifiers_omits_modifiers_key() { + assert_eq!( + LinkDirective::new("Foo", LinkKind::Static).render(), + format!( + "\n#[cfg({NOT_TEST_CFG})]\n#[link(name = \"Foo\", kind = \"static\")]\nunsafe \ + extern \"C\" {{}}" + ) + ); + } + + #[test] + fn render_with_no_bundle_modifier() { + assert_eq!( + LinkDirective::new("Foo", LinkKind::Static) + .with_modifiers(&[LinkModifier::NoBundle]) + .render(), + format!( + "\n#[cfg({NOT_TEST_CFG})]\n#[link(name = \"Foo\", kind = \"static\", \ + modifiers = \"-bundle\")]\nunsafe extern \"C\" {{}}" + ) + ); + } + + #[test] + fn base_wdm_amd64() { + let config = config_for("x86_64", DriverConfig::Wdm); + + assert_eq!( + lib_names(&config.base_libraries()), + ["BufferOverflowFastFailK", "ntoskrnl", "hal", "wmilib"] + ); + } + + #[test] + fn base_wdm_arm64_appends_arm64rt() { + let config = config_for("aarch64", DriverConfig::Wdm); + + assert_eq!( + lib_names(&config.base_libraries()), + [ + "BufferOverflowFastFailK", + "ntoskrnl", + "hal", + "wmilib", + "arm64rt" + ] + ); + } + + #[test] + fn base_kmdf_amd64() { + let config = config_for("x86_64", DriverConfig::Kmdf(KmdfConfig::new())); + + assert_eq!( + lib_names(&config.base_libraries()), + [ + "BufferOverflowFastFailK", + "ntoskrnl", + "hal", + "wmilib", + "WdfLdr", + "WdfDriverEntry" + ] + ); + } + + #[test] + fn base_kmdf_arm64_appends_arm64rt() { + let config = config_for("aarch64", DriverConfig::Kmdf(KmdfConfig::new())); + + assert_eq!( + lib_names(&config.base_libraries()), + [ + "BufferOverflowFastFailK", + "ntoskrnl", + "hal", + "wmilib", + "WdfLdr", + "WdfDriverEntry", + "arm64rt" + ] + ); + } + + #[test] + fn base_umdf_v2_includes_wdf_stub_libs() { + let config = config_for("x86_64", DriverConfig::Umdf(UmdfConfig::new())); + + assert_eq!( + lib_names(&config.base_libraries()), + ["WdfDriverStubUm", "ntdll", "OneCoreUAP"] + ); + } + + #[test] + fn base_umdf_v1_omits_wdf_stub_libs() { + let config = config_for( + "x86_64", + DriverConfig::Umdf(UmdfConfig { + umdf_version_major: 1, + target_umdf_version_minor: 15, + minimum_umdf_version_minor: None, + }), + ); + + assert_eq!(lib_names(&config.base_libraries()), ["OneCoreUAP"]); + } + + #[test] + fn base_umdf_v2_arm64_omits_arm64rt() { + let config = config_for("aarch64", DriverConfig::Umdf(UmdfConfig::new())); + + assert_eq!( + lib_names(&config.base_libraries()), + ["WdfDriverStubUm", "ntdll", "OneCoreUAP"] + ); + } + + #[test] + fn base_umdf_v1_arm64_omits_arm64rt_and_stub_libs() { + let config = config_for( + "aarch64", + DriverConfig::Umdf(UmdfConfig { + umdf_version_major: 1, + target_umdf_version_minor: 15, + minimum_umdf_version_minor: None, + }), + ); + + assert_eq!(lib_names(&config.base_libraries()), ["OneCoreUAP"]); + } + + #[test] + fn base_directives_are_all_static_no_bundle() { + let config = config_for("aarch64", DriverConfig::Kmdf(KmdfConfig::new())); + + let directives = config.base_libraries(); + assert!( + !directives.is_empty(), + "expected base libraries to assert on" + ); + for directive in &directives { + assert_eq!(directive.kind, LinkKind::Static); + assert_eq!(directive.modifiers, [LinkModifier::NoBundle].as_slice()); + } + } + + #[test] + fn hid_wdm_and_kmdf_link_vhfkm_static() { + for driver_config in [DriverConfig::Wdm, DriverConfig::Kmdf(KmdfConfig::new())] { + let config = config_for("x86_64", driver_config); + + assert_eq!( + config.hid_libraries(), + vec![ + LinkDirective::new("VhfKm", LinkKind::Static) + .with_modifiers(&[LinkModifier::NoBundle]) + ] + ); + } + } + + #[test] + fn hid_umdf_links_vhfum_dylib_without_modifiers() { + let config = config_for("x86_64", DriverConfig::Umdf(UmdfConfig::new())); + + assert_eq!( + config.hid_libraries(), + vec![LinkDirective::new("VhfUm", LinkKind::Dylib)] + ); + } + + #[test] + fn libraries_non_contributing_subsets_are_empty() { + let config = config_for("x86_64", DriverConfig::Kmdf(KmdfConfig::new())); + + for subset in [ + ApiSubset::Wdf, + ApiSubset::Gpio, + ApiSubset::ParallelPorts, + ApiSubset::Spb, + ApiSubset::Storage, + ApiSubset::Usb, + ] { + assert!( + config.libraries(subset).is_empty(), + "{subset:?} should contribute no libraries" + ); + } + } + + #[test] + fn libraries_base_subset_matches_base() { + let config = config_for("aarch64", DriverConfig::Kmdf(KmdfConfig::new())); + + assert_eq!(config.libraries(ApiSubset::Base), config.base_libraries()); + } + + #[test] + fn libraries_hid_subset_matches_hid() { + let config = config_for("x86_64", DriverConfig::Umdf(UmdfConfig::new())); + + assert_eq!(config.libraries(ApiSubset::Hid), config.hid_libraries()); + } + + #[test] + fn raw_lines_concatenate_all_libraries_in_subset() { + let config = config_for("x86_64", DriverConfig::Wdm); + + // `bindgen_library_link_raw_lines` is the plain concatenation of each + // library's rendered `#[link]` block (no separators or wrapping), so + // it stays correct regardless of which libraries a subset contributes. + // The exact rendered format is locked by the `render_*` tests, and the + // per-subset library set by the `base_*`/`hid_*` tests. + assert_eq!( + config.bindgen_library_link_raw_lines(ApiSubset::Base), + config + .base_libraries() + .iter() + .map(LinkDirective::render) + .collect::() + ); + } + } + mod validate_and_add_folder_path { use assert_fs::prelude::*; From f0d20e6b9538a5cb050b8a30dc07f205c3d9ebf3 Mon Sep 17 00:00:00 2001 From: Alan Ngo Date: Thu, 2 Jul 2026 09:44:32 -0700 Subject: [PATCH 06/10] Addressing PR comments --- crates/wdk-build/src/lib.rs | 201 +++++++++++-------------------- crates/wdk-sys/build.rs | 51 ++++++-- crates/wdk-sys/src/test_stubs.rs | 4 + 3 files changed, 111 insertions(+), 145 deletions(-) diff --git a/crates/wdk-build/src/lib.rs b/crates/wdk-build/src/lib.rs index b39760784..afd03fe7f 100644 --- a/crates/wdk-build/src/lib.rs +++ b/crates/wdk-build/src/lib.rs @@ -1060,34 +1060,31 @@ impl Config { /// Returns the formatted `#[link]` raw lines for the given [`ApiSubset`], /// ready to be passed to bindgen's `raw_line`. /// - /// This is the link-directive analogue of - /// [`Config::bindgen_header_contents`]: it maps the [`ApiSubset`] (and the - /// active [`DriverConfig`] and [`CpuArchitecture`]) to the native libraries - /// that must be linked, rendering them as `#[link(...)]` attributes emitted - /// into the generated bindings. - /// - /// Unlike header selection, link subsets are selected *non-cumulatively*: - /// each generated bindings file requests only its own subset's libraries - /// (e.g. `hid.rs` requests only [`ApiSubset::Hid`], not - /// [`ApiSubset::Base`]), since the base libraries are already emitted - /// into the base bindings file. This avoids duplicate `#[link]` - /// directives across generated files, and is why this takes a single - /// [`ApiSubset`] rather than a set like - /// [`Config::bindgen_header_contents`]. + /// Each generated bindings file requests only the native libraries + /// introduced by its own [`ApiSubset`]. This avoids duplicate `#[link]` directives + /// across generated files. /// /// Each emitted directive is gated behind /// `#[cfg(not(any(test, feature = "test-stubs")))]`, evaluated in the crate /// that compiles the generated bindings (`wdk-sys`). The directives are /// therefore suppressed when `wdk-sys` is built with its `test-stubs` /// feature, or when `wdk-sys` is itself compiled as a test target (e.g. - /// `cargo test -p wdk-sys`); see the `NOT_TEST_CFG` constant for the - /// rationale. + /// `cargo test -p wdk-sys`). + /// + /// Downstream test executables that depend on `wdk-sys` enable its + /// `test-stubs` feature to provide stubbed symbols. These executables are + /// not driver binaries, so they do not receive the WDK + /// `rustc-link-search` paths that [`Config::configure_binary_build`] emits + /// and must not link the real WDK libraries. The `test` predicate covers + /// the case where `wdk-sys` itself is compiled as a test target. #[must_use] - pub fn bindgen_library_link_raw_lines(&self, api_subset: ApiSubset) -> String { - self.libraries(api_subset) - .iter() - .map(LinkDirective::render) - .collect() + pub fn bindgen_library_link_raw_lines(&self, api_subset: ApiSubset) -> Option { + let libraries = self.libraries(api_subset); + if libraries.is_empty() { + None + } else { + Some(libraries.iter().map(LinkDirective::render).collect()) + } } /// Returns the native libraries that must be linked for a given @@ -1110,25 +1107,21 @@ impl Config { /// Builds the static library link directives for the base bindings. /// + /// Base libraries are derived from WindowsDriver.KernelMode.props + /// (WDM/KMDF) and WindowsDriver.UserMode.props (UMDF) in the Ni(22H2) WDK. + /// ARM64 WDM/KMDF builds also link `arm64rt`, derived from + /// WindowsDriver.arm64.props. + /// /// TODO: Once [link-arg-attribute](https://doc.rust-lang.org/unstable-book/language-features/link-arg-attribute.html) /// stabilizes, the `cargo::rustc-cdylib-link-arg=*` directives emitted by /// [`wdk_build::Config::configure_binary_build`] will be moved here too. - #[tracing::instrument(level = "trace")] fn base_libraries(&self) -> Vec { const fn static_lib(name: &'static str) -> LinkDirective { - // `LinkModifier::NoBundle` (rustc `-bundle`): don't pack the static - // lib into wdk-sys's rlib; defer its linking to the final driver - // binary, which is where the WDK `rustc-link-search` paths are - // emitted (by `Config::configure_binary_build`). Without it, building - // wdk-sys's rlib would fail since those search paths aren't available - // then. - LinkDirective::new(name, LinkKind::Static).with_modifiers(&[LinkModifier::NoBundle]) + LinkDirective::new(name, LinkKind::Static) } let mut directives = Vec::new(); - // Base libraries derived from WindowsDriver.KernelMode.props (WDM/KMDF) - // and WindowsDriver.UserMode.props (UMDF) in the Ni(22H2) WDK. match &self.driver_config { DriverConfig::Wdm => { directives.extend([ @@ -1137,6 +1130,9 @@ impl Config { static_lib("hal"), static_lib("wmilib"), ]); + if self.cpu_architecture == CpuArchitecture::Arm64 { + directives.push(static_lib("arm64rt")); + } } DriverConfig::Kmdf(_) => { directives.extend([ @@ -1147,6 +1143,9 @@ impl Config { static_lib("WdfLdr"), static_lib("WdfDriverEntry"), ]); + if self.cpu_architecture == CpuArchitecture::Arm64 { + directives.push(static_lib("arm64rt")); + } } DriverConfig::Umdf(umdf_config) => { if umdf_config.umdf_version_major >= 2 { @@ -1156,29 +1155,17 @@ impl Config { } } - // ARM64-specific libraries to link to derived from - // WindowsDriver.arm64.props. - if matches!( - self.driver_config, - DriverConfig::Wdm | DriverConfig::Kmdf(_) - ) && self.cpu_architecture == CpuArchitecture::Arm64 - { - directives.push(static_lib("arm64rt")); - } - directives } - #[tracing::instrument(level = "trace")] + /// Builds the Virtual HID Framework library link directives for HID + /// bindings. + /// + /// WDM/KMDF drivers link `VhfKm`, while UMDF drivers link `VhfUm`. fn hid_libraries(&self) -> Vec { - match self.driver_config { + match &self.driver_config { DriverConfig::Wdm | DriverConfig::Kmdf(_) => { - // `LinkModifier::NoBundle` for the same reason as the base - // static libs; see `static_lib` in `base_libraries`. - vec![ - LinkDirective::new("VhfKm", LinkKind::Static) - .with_modifiers(&[LinkModifier::NoBundle]), - ] + vec![LinkDirective::new("VhfKm", LinkKind::Static)] } DriverConfig::Umdf(_) => vec![LinkDirective::new("VhfUm", LinkKind::Dylib)], } @@ -1594,21 +1581,6 @@ pub fn configure_wdk_binary_build() -> Result<(), ConfigError> { Config::from_env_auto()?.configure_binary_build() } -/// `cfg` predicate that gates the WDK link directives so that they are only -/// emitted when the bindings are compiled for a real driver build. -/// -/// Downstream test executables that depend on `wdk-sys` enable its `test-stubs` -/// feature (which provides stubbed symbols *and*, via this gate, suppresses -/// real WDK library linkage; see `wdk_sys::test_stubs`). Such executables are -/// not driver binaries, so they never receive the WDK `rustc-link-search` paths -/// that `Config::configure_binary_build` emits, and must therefore not attempt -/// to link the real WDK libraries. -/// -/// The `test` predicate covers the analogous case where `wdk-sys` *itself* is -/// compiled as a test target (e.g. `cargo test -p wdk-sys`): that build goes -/// through `configure_library_build` rather than `configure_binary_build`, so -/// it likewise lacks the `rustc-link-search` paths and must not link the real -/// WDK libraries. const NOT_TEST_CFG: &str = r#"not(any(test, feature = "test-stubs"))"#; /// The `kind` of a `#[link]` attribute (i.e. how the library is linked). @@ -1636,55 +1608,26 @@ impl LinkKind { } } -/// A linking modifier for a `#[link]` attribute (the `modifiers = "..."` -/// field). Only the modifiers actually emitted by [`Config`] are modelled. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum LinkModifier { - /// `-bundle` - NoBundle, -} - -impl LinkModifier { - /// Returns the string used in the `modifiers = "..."` field of a `#[link]` - /// attribute, including its `+`/`-` prefix. - const fn as_str(self) -> &'static str { - match self { - Self::NoBundle => "-bundle", - } - } -} - /// A native library to link into the generated bindings. /// /// Rendered by [`LinkDirective::render`] as a `#[link(...)]` attribute on an /// (empty) `extern` block and emitted into the bindgen output via bindgen's /// `raw_line`. Every directive is gated behind [`NOT_TEST_CFG`] so that -/// non-driver test consumers don't link the real WDK libraries. +/// non-driver test consumers don't link the real WDK libraries. Static +/// libraries are emitted with `modifiers = "-bundle"` so they are linked into +/// the final driver binary instead of bundled into the `wdk-sys` rlib. #[derive(Debug, Clone, PartialEq, Eq)] struct LinkDirective { /// `name = "..."` — the library to link. name: &'static str, /// `kind = "..."` — how the library is linked. kind: LinkKind, - /// `modifiers = "..."` values (e.g. [`LinkModifier::NoBundle`]). Joined - /// with commas; an empty slice omits the `modifiers` key entirely. - modifiers: &'static [LinkModifier], } impl LinkDirective { - /// Creates a directive with no modifiers. + /// Creates a link directive. const fn new(name: &'static str, kind: LinkKind) -> Self { - Self { - name, - kind, - modifiers: &[], - } - } - - /// Sets the linking modifiers (e.g. `[LinkModifier::NoBundle]`). - const fn with_modifiers(mut self, modifiers: &'static [LinkModifier]) -> Self { - self.modifiers = modifiers; - self + Self { name, kind } } /// Renders this directive into a self-contained block of Rust source. @@ -1692,21 +1635,13 @@ impl LinkDirective { /// Emitted as a single block so the attributes always stay attached to the /// following `extern` block, regardless of where bindgen places raw lines. fn render(&self) -> String { - let modifiers = if self.modifiers.is_empty() { - String::new() - } else { - let modifiers = self - .modifiers - .iter() - .map(|modifier| modifier.as_str()) - .collect::>() - .join(","); - format!(r#", modifiers = "{modifiers}""#) + let modifiers = match self.kind { + LinkKind::Static => r#", modifiers = "-bundle""#, + LinkKind::Dylib => "", }; format!( - r#" -#[cfg({cfg})] + r#"#[cfg({cfg})] #[link(name = "{name}", kind = "{kind}"{modifiers})] unsafe extern "C" {{}}"#, cfg = NOT_TEST_CFG, @@ -2298,35 +2233,30 @@ mod tests { } #[test] - fn render_minimal_directive() { + fn link_directives_are_disabled_for_tests_and_test_stubs() { assert_eq!( - LinkDirective::new("Foo", LinkKind::Dylib).render(), - format!( - "\n#[cfg({NOT_TEST_CFG})]\n#[link(name = \"Foo\", kind = \"dylib\")]\nunsafe \ - extern \"C\" {{}}" - ) + NOT_TEST_CFG, + r#"not(any(test, feature = "test-stubs"))"# ); } #[test] - fn render_static_without_modifiers_omits_modifiers_key() { + fn render_minimal_directive() { assert_eq!( - LinkDirective::new("Foo", LinkKind::Static).render(), + LinkDirective::new("Foo", LinkKind::Dylib).render(), format!( - "\n#[cfg({NOT_TEST_CFG})]\n#[link(name = \"Foo\", kind = \"static\")]\nunsafe \ + "#[cfg({NOT_TEST_CFG})]\n#[link(name = \"Foo\", kind = \"dylib\")]\nunsafe \ extern \"C\" {{}}" ) ); } #[test] - fn render_with_no_bundle_modifier() { + fn render_static_directive_includes_no_bundle_modifier() { assert_eq!( - LinkDirective::new("Foo", LinkKind::Static) - .with_modifiers(&[LinkModifier::NoBundle]) - .render(), + LinkDirective::new("Foo", LinkKind::Static).render(), format!( - "\n#[cfg({NOT_TEST_CFG})]\n#[link(name = \"Foo\", kind = \"static\", \ + "#[cfg({NOT_TEST_CFG})]\n#[link(name = \"Foo\", kind = \"static\", \ modifiers = \"-bundle\")]\nunsafe extern \"C\" {{}}" ) ); @@ -2452,7 +2382,10 @@ mod tests { ); for directive in &directives { assert_eq!(directive.kind, LinkKind::Static); - assert_eq!(directive.modifiers, [LinkModifier::NoBundle].as_slice()); + assert!( + directive.render().contains(r#"modifiers = "-bundle""#), + "static directive should render with -bundle" + ); } } @@ -2463,10 +2396,7 @@ mod tests { assert_eq!( config.hid_libraries(), - vec![ - LinkDirective::new("VhfKm", LinkKind::Static) - .with_modifiers(&[LinkModifier::NoBundle]) - ] + vec![LinkDirective::new("VhfKm", LinkKind::Static)] ); } } @@ -2497,6 +2427,7 @@ mod tests { config.libraries(subset).is_empty(), "{subset:?} should contribute no libraries" ); + assert_eq!(config.bindgen_library_link_raw_lines(subset), None); } } @@ -2525,11 +2456,13 @@ mod tests { // per-subset library set by the `base_*`/`hid_*` tests. assert_eq!( config.bindgen_library_link_raw_lines(ApiSubset::Base), - config - .base_libraries() - .iter() - .map(LinkDirective::render) - .collect::() + Some( + config + .base_libraries() + .iter() + .map(LinkDirective::render) + .collect::() + ) ); } } diff --git a/crates/wdk-sys/build.rs b/crates/wdk-sys/build.rs index 7ccaa52fc..77c9e5027 100644 --- a/crates/wdk-sys/build.rs +++ b/crates/wdk-sys/build.rs @@ -269,10 +269,15 @@ fn generate_base(out_path: &Path, config: &Config) -> Result<(), ConfigError> { let header_contents = config.bindgen_header_contents([ApiSubset::Base])?; trace!(header_contents = ?header_contents); - let bindgen_builder = bindgen::Builder::wdk_default(config)? - .with_codegen_config((CodegenConfig::TYPES | CodegenConfig::VARS).complement()) - .header_contents(&format!("{outfile_name}-input.h"), &header_contents) - .raw_line(config.bindgen_library_link_raw_lines(ApiSubset::Base)); + let bindgen_builder = { + let mut builder = bindgen::Builder::wdk_default(config)? + .with_codegen_config((CodegenConfig::TYPES | CodegenConfig::VARS).complement()) + .header_contents(&format!("{outfile_name}-input.h"), &header_contents); + if let Some(raw_lines) = config.bindgen_library_link_raw_lines(ApiSubset::Base) { + builder = builder.raw_line(raw_lines); + } + builder + }; trace!(bindgen_builder = ?bindgen_builder); let output_file_path = out_path.join(format!("{outfile_name}.rs")); @@ -290,12 +295,18 @@ fn generate_wdf(out_path: &Path, config: &Config) -> Result<(), ConfigError> { let header_contents = config.bindgen_header_contents([ApiSubset::Base, ApiSubset::Wdf])?; trace!(header_contents = ?header_contents); - let bindgen_builder = bindgen::Builder::wdk_default(config)? - .with_codegen_config((CodegenConfig::TYPES | CodegenConfig::VARS).complement()) - .header_contents("wdf-input.h", &header_contents) - // Only generate for files that are prefixed with (case-insensitive) wdf (ie. - // /some/path/WdfSomeHeader.h), to prevent duplication of code in ntddk.rs - .allowlist_file("(?i).*wdf.*"); + let bindgen_builder = { + let mut builder = bindgen::Builder::wdk_default(config)? + .with_codegen_config((CodegenConfig::TYPES | CodegenConfig::VARS).complement()) + .header_contents("wdf-input.h", &header_contents) + // Only generate for files that are prefixed with (case-insensitive) wdf (ie. + // /some/path/WdfSomeHeader.h), to prevent duplication of code in ntddk.rs + .allowlist_file("(?i).*wdf.*"); + if let Some(raw_lines) = config.bindgen_library_link_raw_lines(ApiSubset::Wdf) { + builder = builder.raw_line(raw_lines); + } + builder + }; trace!(bindgen_builder = ?bindgen_builder); let output_file_path = out_path.join("wdf.rs"); @@ -331,6 +342,9 @@ fn generate_gpio(out_path: &Path, config: &Config) -> Result<(), ConfigError> { for header_file in config.headers(ApiSubset::Gpio)? { builder = builder.allowlist_file(format!("(?i).*{header_file}.*")); } + if let Some(raw_lines) = config.bindgen_library_link_raw_lines(ApiSubset::Gpio) { + builder = builder.raw_line(raw_lines); + } builder }; trace!(bindgen_builder = ?bindgen_builder); @@ -362,7 +376,10 @@ fn generate_hid(out_path: &Path, config: &Config) -> Result<(), ConfigError> { builder = builder.allowlist_file(format!("(?i).*{header_file}.*")); } - builder.raw_line(config.bindgen_library_link_raw_lines(ApiSubset::Hid)) + if let Some(raw_lines) = config.bindgen_library_link_raw_lines(ApiSubset::Hid) { + builder = builder.raw_line(raw_lines); + } + builder }; trace!(bindgen_builder = ?bindgen_builder); @@ -395,6 +412,9 @@ fn generate_parallel_ports(out_path: &Path, config: &Config) -> Result<(), Confi for header_file in config.headers(ApiSubset::ParallelPorts)? { builder = builder.allowlist_file(format!("(?i).*{header_file}.*")); } + if let Some(raw_lines) = config.bindgen_library_link_raw_lines(ApiSubset::ParallelPorts) { + builder = builder.raw_line(raw_lines); + } builder }; trace!(bindgen_builder = ?bindgen_builder); @@ -425,6 +445,9 @@ fn generate_spb(out_path: &Path, config: &Config) -> Result<(), ConfigError> { for header_file in config.headers(ApiSubset::Spb)? { builder = builder.allowlist_file(format!("(?i).*{header_file}.*")); } + if let Some(raw_lines) = config.bindgen_library_link_raw_lines(ApiSubset::Spb) { + builder = builder.raw_line(raw_lines); + } builder }; trace!(bindgen_builder = ?bindgen_builder); @@ -455,6 +478,9 @@ fn generate_storage(out_path: &Path, config: &Config) -> Result<(), ConfigError> for header_file in config.headers(ApiSubset::Storage)? { builder = builder.allowlist_file(format!("(?i).*{header_file}.*")); } + if let Some(raw_lines) = config.bindgen_library_link_raw_lines(ApiSubset::Storage) { + builder = builder.raw_line(raw_lines); + } builder }; trace!(bindgen_builder = ?bindgen_builder); @@ -485,6 +511,9 @@ fn generate_usb(out_path: &Path, config: &Config) -> Result<(), ConfigError> { for header_file in config.headers(ApiSubset::Usb)? { builder = builder.allowlist_file(format!("(?i).*{header_file}.*")); } + if let Some(raw_lines) = config.bindgen_library_link_raw_lines(ApiSubset::Usb) { + builder = builder.raw_line(raw_lines); + } builder }; trace!(bindgen_builder = ?bindgen_builder); diff --git a/crates/wdk-sys/src/test_stubs.rs b/crates/wdk-sys/src/test_stubs.rs index ff301dc38..2692dc80f 100644 --- a/crates/wdk-sys/src/test_stubs.rs +++ b/crates/wdk-sys/src/test_stubs.rs @@ -6,6 +6,10 @@ //! //! These stubs can be brought into scope by introducing `wdk-sys` with the //! `test-stubs` feature in the `dev-dependencies` of the crate's `Cargo.toml` +//! +//! This feature is intended for test builds only. It also suppresses generated +//! WDK native library link directives, so tests must not call real WDK APIs +//! unless those APIs are separately stubbed, mocked, or otherwise provided. #[cfg(any(driver_model__driver_type = "KMDF", driver_model__driver_type = "UMDF"))] pub use wdf::*; From 9e6f1c8badef4bb4289aa5f3e1acac9be2f0ad81 Mon Sep 17 00:00:00 2001 From: Alan Ngo Date: Thu, 2 Jul 2026 09:50:45 -0700 Subject: [PATCH 07/10] formatting --- crates/wdk-build/src/lib.rs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/crates/wdk-build/src/lib.rs b/crates/wdk-build/src/lib.rs index afd03fe7f..9449b3282 100644 --- a/crates/wdk-build/src/lib.rs +++ b/crates/wdk-build/src/lib.rs @@ -1061,8 +1061,8 @@ impl Config { /// ready to be passed to bindgen's `raw_line`. /// /// Each generated bindings file requests only the native libraries - /// introduced by its own [`ApiSubset`]. This avoids duplicate `#[link]` directives - /// across generated files. + /// introduced by its own [`ApiSubset`]. This avoids duplicate `#[link]` + /// directives across generated files. /// /// Each emitted directive is gated behind /// `#[cfg(not(any(test, feature = "test-stubs")))]`, evaluated in the crate @@ -2234,10 +2234,7 @@ mod tests { #[test] fn link_directives_are_disabled_for_tests_and_test_stubs() { - assert_eq!( - NOT_TEST_CFG, - r#"not(any(test, feature = "test-stubs"))"# - ); + assert_eq!(NOT_TEST_CFG, r#"not(any(test, feature = "test-stubs"))"#); } #[test] @@ -2256,8 +2253,8 @@ mod tests { assert_eq!( LinkDirective::new("Foo", LinkKind::Static).render(), format!( - "#[cfg({NOT_TEST_CFG})]\n#[link(name = \"Foo\", kind = \"static\", \ - modifiers = \"-bundle\")]\nunsafe extern \"C\" {{}}" + "#[cfg({NOT_TEST_CFG})]\n#[link(name = \"Foo\", kind = \"static\", modifiers \ + = \"-bundle\")]\nunsafe extern \"C\" {{}}" ) ); } From 8348a3f2c4110b2cb4ad9ee8778ebaf10d941d98 Mon Sep 17 00:00:00 2001 From: Alan Ngo Date: Mon, 6 Jul 2026 12:41:09 -0700 Subject: [PATCH 08/10] - add new line after unsafe block in link directive return string - add doctest to wdk-sys-tests - add cdylib driver unit test to mixed-package-kmdf-workspace, and update Cargo.toml --- crates/wdk-build/src/lib.rs | 3 +- .../crates/driver/Cargo.toml | 5 ++- .../crates/driver/src/lib.rs | 38 ++++++++++++++++++- tests/wdk-sys-tests/src/lib.rs | 23 +++++++++++ 4 files changed, 65 insertions(+), 4 deletions(-) diff --git a/crates/wdk-build/src/lib.rs b/crates/wdk-build/src/lib.rs index 9449b3282..c29507a5f 100644 --- a/crates/wdk-build/src/lib.rs +++ b/crates/wdk-build/src/lib.rs @@ -1643,7 +1643,8 @@ impl LinkDirective { format!( r#"#[cfg({cfg})] #[link(name = "{name}", kind = "{kind}"{modifiers})] -unsafe extern "C" {{}}"#, +unsafe extern "C" {{}} +"#, cfg = NOT_TEST_CFG, name = self.name, kind = self.kind.as_str(), diff --git a/tests/mixed-package-kmdf-workspace/crates/driver/Cargo.toml b/tests/mixed-package-kmdf-workspace/crates/driver/Cargo.toml index 763f536a3..63b8924a6 100644 --- a/tests/mixed-package-kmdf-workspace/crates/driver/Cargo.toml +++ b/tests/mixed-package-kmdf-workspace/crates/driver/Cargo.toml @@ -9,8 +9,6 @@ version = "0.0.0" [lib] crate-type = ["cdylib"] -# temporarily disable due to github runner issues: https://github.com/microsoft/windows-drivers-rs/issues/502 -test = false [build-dependencies] wdk-build.workspace = true @@ -20,3 +18,6 @@ wdk.workspace = true wdk-alloc.workspace = true wdk-panic.workspace = true wdk-sys.workspace = true + +[dev-dependencies] +wdk-sys = { workspace = true, features = ["test-stubs"] } diff --git a/tests/mixed-package-kmdf-workspace/crates/driver/src/lib.rs b/tests/mixed-package-kmdf-workspace/crates/driver/src/lib.rs index 20b297dfd..314ef57e2 100644 --- a/tests/mixed-package-kmdf-workspace/crates/driver/src/lib.rs +++ b/tests/mixed-package-kmdf-workspace/crates/driver/src/lib.rs @@ -5,23 +5,35 @@ //! //! This is a sample KMDF driver that demonstrates how to use the crates in //! windows-driver-rs to create a skeleton of a kmdf driver. +//! +//! Running `cargo test` the crate is built as a std test harness with +//! `wdk-sys`'s `test-stubs` feature enabled (see dev-dependencies): that +//! suppresses the generated WDK `#[link]` directives and `wdk_sys::test_stubs` +//! supplies the `DriverEntry`/`WdfFunctions` symbols. The harness links and +//! runs in user mode without pulling in KM libs. The driver's +//! own `DriverEntry` MUST be `#[cfg(not(test))]`, otherwise +//! it collides with the stub's `DriverEntry`. -#![no_std] +#![cfg_attr(not(test), no_std)] +#[cfg(not(test))] extern crate alloc; #[cfg(not(test))] extern crate wdk_panic; +#[cfg(not(test))] use alloc::{ ffi::CString, slice, string::String, }; +#[cfg(not(test))] use wdk::println; #[cfg(not(test))] use wdk_alloc::WdkAllocator; +#[cfg(not(test))] use wdk_sys::{ call_unsafe_wdf_function_binding, ntddk::DbgPrint, @@ -53,6 +65,7 @@ static GLOBAL_ALLOCATOR: WdkAllocator = WdkAllocator; /// Function is unsafe since it dereferences raw pointers passed to it from WDF // SAFETY: "DriverEntry" is the required symbol name for Windows driver entry points. // No other function in this compilation unit exports this name, preventing symbol conflicts. +#[cfg(not(test))] #[unsafe(export_name = "DriverEntry")] // WDF expects a symbol with the name DriverEntry pub unsafe extern "system" fn driver_entry( driver: &mut DRIVER_OBJECT, @@ -156,6 +169,7 @@ pub unsafe extern "system" fn driver_entry( wdf_driver_create_ntstatus } +#[cfg(not(test))] extern "C" fn evt_driver_device_add( _driver: WDFDRIVER, mut device_init: *mut WDFDEVICE_INIT, @@ -182,3 +196,25 @@ extern "C" fn evt_driver_device_add( println!("WdfDeviceCreate NTSTATUS: {ntstatus:#02x}"); ntstatus } + +#[cfg(test)] +mod tests { + use wdk_sys::{ULONG, WDF_DRIVER_CONFIG}; + + /// Checks a real invariant the driver's `DriverEntry` relies on: it stores + /// `size_of::()` into the `ULONG` `Size` field (see the + /// `const assert!` in `driver_entry`), so the size must fit in a `ULONG` + /// for the KMDF configuration this crate is built against. + /// + /// The value is that this *builds, links, and runs*, proving a + /// `wdk-sys`-dependent driver cdylib can host unit tests. The driver's + /// `[dev-dependencies]` enable `wdk-sys`'s `test-stubs` feature, whose arm of + /// the `#[cfg(not(any(test, feature = "test-stubs")))]` gate suppresses the + /// generated WDK `#[link]` directives, so this user-mode exe links without + /// kernel-mode libraries. + #[test] + fn wdf_driver_config_size_fits_ulong() { + // Checks a type generated from bindgen and not an FFI + assert!(core::mem::size_of::() <= ULONG::MAX as usize); + } +} diff --git a/tests/wdk-sys-tests/src/lib.rs b/tests/wdk-sys-tests/src/lib.rs index 305949605..822f53e2b 100644 --- a/tests/wdk-sys-tests/src/lib.rs +++ b/tests/wdk-sys-tests/src/lib.rs @@ -1,2 +1,25 @@ // Copyright (c) Microsoft Corporation // License: MIT OR Apache-2.0 + +//! Tests for `wdk-sys` that rely on a WDK configuration being +//! present in the build graph. +//! +//! The crate-level example below doubles as a **doctest**. +//! `cargo test` compiles and links each doctest as its own user-mode binary. It +//! links against `wdk-sys`, which is pulled in here with the `test-stubs` +//! feature (see this crate's dependencies). That feature cfg-suppresses the +//! generated WDK `#[link]` directives, so the doctest links and runs without +//! pulling kernel-mode libraries into it. +//! +//! Like the driver unit test in `tests/mixed-package-kmdf-workspace`, it +//! references a bindgen generated WDM type (`DRIVER_OBJECT`) via a const +//! `size_of` — so the doctest actually links `wdk-sys`'s generated bindings +//! while touching only a type (never a KM function +//! binding that `test-stubs` leaves unlinked). +//! +//! ``` +//! use wdk_sys::{DRIVER_OBJECT, NT_SUCCESS, STATUS_SUCCESS, ULONG}; +//! +//! assert!(NT_SUCCESS(STATUS_SUCCESS)); +//! assert!(core::mem::size_of::() <= ULONG::MAX as usize); +//! ``` From 082282c825adafae5b595b8b8b79d6aa275acac2 Mon Sep 17 00:00:00 2001 From: Alan Ngo Date: Mon, 6 Jul 2026 14:01:52 -0700 Subject: [PATCH 09/10] forgot to update unit tests with change to render function --- crates/wdk-build/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/wdk-build/src/lib.rs b/crates/wdk-build/src/lib.rs index c29507a5f..364a59af3 100644 --- a/crates/wdk-build/src/lib.rs +++ b/crates/wdk-build/src/lib.rs @@ -2244,7 +2244,7 @@ mod tests { LinkDirective::new("Foo", LinkKind::Dylib).render(), format!( "#[cfg({NOT_TEST_CFG})]\n#[link(name = \"Foo\", kind = \"dylib\")]\nunsafe \ - extern \"C\" {{}}" + extern \"C\" {{}}\n" ) ); } @@ -2255,7 +2255,7 @@ mod tests { LinkDirective::new("Foo", LinkKind::Static).render(), format!( "#[cfg({NOT_TEST_CFG})]\n#[link(name = \"Foo\", kind = \"static\", modifiers \ - = \"-bundle\")]\nunsafe extern \"C\" {{}}" + = \"-bundle\")]\nunsafe extern \"C\" {{}}\n" ) ); } From 0b5deedcef962852858667be1b4dca4b1be391be Mon Sep 17 00:00:00 2001 From: Alan Ngo Date: Mon, 6 Jul 2026 14:04:39 -0700 Subject: [PATCH 10/10] toml formatting --- tests/mixed-package-kmdf-workspace/crates/driver/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/mixed-package-kmdf-workspace/crates/driver/Cargo.toml b/tests/mixed-package-kmdf-workspace/crates/driver/Cargo.toml index 63b8924a6..a1b5ec40f 100644 --- a/tests/mixed-package-kmdf-workspace/crates/driver/Cargo.toml +++ b/tests/mixed-package-kmdf-workspace/crates/driver/Cargo.toml @@ -20,4 +20,4 @@ wdk-panic.workspace = true wdk-sys.workspace = true [dev-dependencies] -wdk-sys = { workspace = true, features = ["test-stubs"] } +wdk-sys = { features = ["test-stubs"], workspace = true }