diff --git a/crates/wdk-build/src/lib.rs b/crates/wdk-build/src/lib.rs index 710708e5b..364a59af3 100644 --- a/crates/wdk-build/src/lib.rs +++ b/crates/wdk-build/src/lib.rs @@ -1057,6 +1057,120 @@ impl Config { .collect()) } + /// Returns the formatted `#[link]` raw lines for the given [`ApiSubset`], + /// 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. + /// + /// 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`). + /// + /// 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) -> 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 + /// [`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_libraries(), + ApiSubset::Hid => self.hid_libraries(), + ApiSubset::Wdf + | ApiSubset::Gpio + | ApiSubset::ParallelPorts + | ApiSubset::Spb + | ApiSubset::Storage + | ApiSubset::Usb => Vec::new(), + } + } + + /// 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. + fn base_libraries(&self) -> Vec { + const fn static_lib(name: &'static str) -> LinkDirective { + LinkDirective::new(name, LinkKind::Static) + } + + 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"), + ]); + if self.cpu_architecture == CpuArchitecture::Arm64 { + directives.push(static_lib("arm64rt")); + } + } + DriverConfig::Kmdf(_) => { + directives.extend([ + static_lib("BufferOverflowFastFailK"), + static_lib("ntoskrnl"), + static_lib("hal"), + static_lib("wmilib"), + 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 { + directives.extend([static_lib("WdfDriverStubUm"), static_lib("ntdll")]); + } + directives.push(static_lib("OneCoreUAP")); + } + } + + directives + } + + /// 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 { + DriverConfig::Wdm | DriverConfig::Kmdf(_) => { + vec![LinkDirective::new("VhfKm", LinkKind::Static)] + } + DriverConfig::Umdf(_) => vec![LinkDirective::new("VhfUm", LinkKind::Dylib)], + } + } + /// Configure a Cargo build of a library that depends on the WDK. This /// emits specially formatted prints to Cargo based on this [`Config`]. /// @@ -1125,20 +1239,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"); - } - + // 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"); @@ -1159,20 +1265,7 @@ impl Config { println!("cargo::rustc-cdylib-link-arg=/IGNORE:4216"); } 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"); - } - + // 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"); @@ -1187,16 +1280,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"); @@ -1495,6 +1581,77 @@ pub fn configure_wdk_binary_build() -> Result<(), ConfigError> { Config::from_env_auto()?.configure_binary_build() } +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). +/// +/// 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, PartialEq, Eq)] +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 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. 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, +} + +impl LinkDirective { + /// Creates a link directive. + const fn new(name: &'static str, kind: LinkKind) -> Self { + Self { name, kind } + } + + /// 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 { + let modifiers = match self.kind { + LinkKind::Static => r#", modifiers = "-bundle""#, + LinkKind::Dylib => "", + }; + + format!( + r#"#[cfg({cfg})] +#[link(name = "{name}", kind = "{kind}"{modifiers})] +unsafe extern "C" {{}} +"#, + cfg = NOT_TEST_CFG, + name = self.name, + kind = self.kind.as_str(), + ) + } +} + /// 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 @@ -2050,6 +2207,264 @@ 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 link_directives_are_disabled_for_tests_and_test_stubs() { + assert_eq!(NOT_TEST_CFG, r#"not(any(test, feature = "test-stubs"))"#); + } + + #[test] + fn render_minimal_directive() { + assert_eq!( + LinkDirective::new("Foo", LinkKind::Dylib).render(), + format!( + "#[cfg({NOT_TEST_CFG})]\n#[link(name = \"Foo\", kind = \"dylib\")]\nunsafe \ + extern \"C\" {{}}\n" + ) + ); + } + + #[test] + fn render_static_directive_includes_no_bundle_modifier() { + assert_eq!( + LinkDirective::new("Foo", LinkKind::Static).render(), + format!( + "#[cfg({NOT_TEST_CFG})]\n#[link(name = \"Foo\", kind = \"static\", modifiers \ + = \"-bundle\")]\nunsafe extern \"C\" {{}}\n" + ) + ); + } + + #[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!( + directive.render().contains(r#"modifiers = "-bundle""#), + "static directive should render with -bundle" + ); + } + } + + #[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)] + ); + } + } + + #[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" + ); + assert_eq!(config.bindgen_library_link_raw_lines(subset), None); + } + } + + #[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), + Some( + config + .base_libraries() + .iter() + .map(LinkDirective::render) + .collect::() + ) + ); + } + } + mod validate_and_add_folder_path { use assert_fs::prelude::*; diff --git a/crates/wdk-sys/build.rs b/crates/wdk-sys/build.rs index 2a7308487..77c9e5027 100644 --- a/crates/wdk-sys/build.rs +++ b/crates/wdk-sys/build.rs @@ -269,9 +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); + 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")); @@ -289,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"); @@ -330,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); @@ -360,6 +375,10 @@ 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}.*")); } + + 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); @@ -393,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); @@ -423,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); @@ -453,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); @@ -483,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::*; diff --git a/tests/mixed-package-kmdf-workspace/crates/driver/Cargo.toml b/tests/mixed-package-kmdf-workspace/crates/driver/Cargo.toml index 763f536a3..a1b5ec40f 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 = { features = ["test-stubs"], workspace = true } 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/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" } 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); +//! ```