diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bced0c464e..a8fd240ee9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -192,4 +192,5 @@ jobs: cargo clippy -p tool_msvc && cargo clippy -p tool_sys && cargo clippy -p tool_windows && - cargo clippy -p tool_yml + cargo clippy -p tool_yml && + cargo clippy -p test_debugger_visualizer diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5eca39c87a..d6202ccaf6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -178,6 +178,10 @@ jobs: cargo test --target ${{ matrix.target }} -p tool_windows && cargo test --target ${{ matrix.target }} -p tool_yml + - name: Test debugger_visualizer feature + run: cargo test --target ${{ matrix.target }} -p test_debugger_visualizer -- --test-threads=1 + if: matrix.version == 'nightly' && endsWith(matrix.target, '-msvc') + - name: Check import libs shell: pwsh run: | diff --git a/crates/libs/windows/Cargo.toml b/crates/libs/windows/Cargo.toml index 01f305f461..1c64bbb570 100644 --- a/crates/libs/windows/Cargo.toml +++ b/crates/libs/windows/Cargo.toml @@ -724,3 +724,8 @@ Win32_UI_WindowsAndMessaging = ["Win32_UI"] Win32_UI_Wpf = ["Win32_UI"] Win32_UI_Xaml = ["Win32_UI"] Win32_UI_Xaml_Diagnostics = ["Win32_UI_Xaml"] + +# UNSTABLE FEATURES (requires Rust nightly) +# Enable to use the #[debugger_visualizer] attribute. +debugger_visualizer = [] + diff --git a/crates/libs/windows/src/lib.rs b/crates/libs/windows/src/lib.rs index b04813016e..5310493011 100644 --- a/crates/libs/windows/src/lib.rs +++ b/crates/libs/windows/src/lib.rs @@ -4,6 +4,7 @@ Learn more about Rust for Windows here: + + {{ len={len} }} + + + len + + len + data + + + + + + + code + info + + + + + {(HRESULT)__0} + + + + {__0} + + + + "" + {((char16_t*)__0->data),[__0->len]su} + + + __0 == nullptr ? (unsigned int)0 : __0->len + __0->count + __0->flags + + + + __0->len + (char16_t*)__0->data + + + + + + + + + + {(char*)__0,[len()]s8} + + len() + + + + len() + (char*)__0 + + + + + + + + + + {(char16_t*)__0,[len()]su} + + + len() + + + + len() + (char16_t*)__0 + + + + + + diff --git a/crates/tests/debugger_visualizer/Cargo.toml b/crates/tests/debugger_visualizer/Cargo.toml new file mode 100644 index 0000000000..36b6cdb5bb --- /dev/null +++ b/crates/tests/debugger_visualizer/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "test_debugger_visualizer" +version = "0.0.0" +authors = ["Microsoft"] +edition = "2018" + +[dependencies.windows] +path = "../../libs/windows" +features = [ + "debugger_visualizer", + "implement", + "Win32_System_Com", + "Win32_Foundation", +] + +[dev-dependencies] +debugger_test = "0.1.0" +debugger_test_parser = "0.1.0" diff --git a/crates/tests/debugger_visualizer/src/lib.rs b/crates/tests/debugger_visualizer/src/lib.rs new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/crates/tests/debugger_visualizer/src/lib.rs @@ -0,0 +1 @@ + diff --git a/crates/tests/debugger_visualizer/tests/test.rs b/crates/tests/debugger_visualizer/tests/test.rs new file mode 100644 index 0000000000..e93699733c --- /dev/null +++ b/crates/tests/debugger_visualizer/tests/test.rs @@ -0,0 +1,241 @@ +use debugger_test::*; +use windows::core::*; +use windows::Win32::Foundation::*; +use windows::Win32::System::Com::*; + +#[inline(never)] +fn __break() {} + +#[implement(IErrorInfo)] +struct Test; + +impl IErrorInfo_Impl for Test { + fn GetGUID(&self) -> Result { + Err(Error::new(E_OUTOFMEMORY, "Out of memory message".into())) + } + fn GetSource(&self) -> Result { + Err(Error::new(E_INVALIDARG, "Invalid argument message".into())) + } + fn GetDescription(&self) -> Result { + Ok(BSTR::new()) + } + fn GetHelpFile(&self) -> Result { + Ok(BSTR::new()) + } + fn GetHelpContext(&self) -> Result { + Ok(1) + } +} + +#[debugger_test( + debugger = "cdb", + commands = " +.nvlist +dx array + +dx -r2 pstr +dx -r2 pcstr +dx -r2 pwstr +dx -r2 pcwstr + +dx empty +dx -r2 hstring + +dx out_of_memory_error +dx invalid_argument_error + ", + expected_statements = r#" +array : { len=0xd } [Type: windows::core::array::Array] + [] [Type: windows::core::array::Array] + [len] : 0xd + [0] : 0x48 [Type: unsigned char] + [1] : 0x65 [Type: unsigned char] + [2] : 0x6c [Type: unsigned char] + [3] : 0x6c [Type: unsigned char] + [4] : 0x6f [Type: unsigned char] + [5] : 0x20 [Type: unsigned char] + [6] : 0x57 [Type: unsigned char] + [7] : 0x6f [Type: unsigned char] + [8] : 0x72 [Type: unsigned char] + [9] : 0x6c [Type: unsigned char] + [10] : 0x64 [Type: unsigned char] + [11] : 0x21 [Type: unsigned char] + [12] : 0x0 [Type: unsigned char] + +pstr : "This is a PSTR" [Type: windows::core::strings::pstr::PSTR] + [] [Type: windows::core::strings::pstr::PSTR] + [len] : 0xe + [chars] + [0] : 84 'T' [Type: char] + [1] : 104 'h' [Type: char] + [2] : 105 'i' [Type: char] + [3] : 115 's' [Type: char] + [4] : 32 ' ' [Type: char] + [5] : 105 'i' [Type: char] + [6] : 115 's' [Type: char] + [7] : 32 ' ' [Type: char] + [8] : 97 'a' [Type: char] + [9] : 32 ' ' [Type: char] + [10] : 80 'P' [Type: char] + [11] : 83 'S' [Type: char] + [12] : 84 'T' [Type: char] + [13] : 82 'R' [Type: char] + +pcstr : "This is a PCSTR" [Type: windows::core::strings::pcstr::PCSTR] + [] [Type: windows::core::strings::pcstr::PCSTR] + [len] : 0xf + [chars] + [0] : 84 'T' [Type: char] + [1] : 104 'h' [Type: char] + [2] : 105 'i' [Type: char] + [3] : 115 's' [Type: char] + [4] : 32 ' ' [Type: char] + [5] : 105 'i' [Type: char] + [6] : 115 's' [Type: char] + [7] : 32 ' ' [Type: char] + [8] : 97 'a' [Type: char] + [9] : 32 ' ' [Type: char] + [10] : 80 'P' [Type: char] + [11] : 67 'C' [Type: char] + [12] : 83 'S' [Type: char] + [13] : 84 'T' [Type: char] + [14] : 82 'R' [Type: char] + +pwstr : "This is a PWSTR" [Type: windows::core::strings::pwstr::PWSTR] + [] [Type: windows::core::strings::pwstr::PWSTR] + [len] : 0xf + [chars] + [0] : 0x54 'T' [Type: char16_t] + [1] : 0x68 'h' [Type: char16_t] + [2] : 0x69 'i' [Type: char16_t] + [3] : 0x73 's' [Type: char16_t] + [4] : 0x20 ' ' [Type: char16_t] + [5] : 0x69 'i' [Type: char16_t] + [6] : 0x73 's' [Type: char16_t] + [7] : 0x20 ' ' [Type: char16_t] + [8] : 0x61 'a' [Type: char16_t] + [9] : 0x20 ' ' [Type: char16_t] + [10] : 0x50 'P' [Type: char16_t] + [11] : 0x57 'W' [Type: char16_t] + [12] : 0x53 'S' [Type: char16_t] + [13] : 0x54 'T' [Type: char16_t] + [14] : 0x52 'R' [Type: char16_t] + +pcwstr : "This is a PCWSTR" [Type: windows::core::strings::pcwstr::PCWSTR] + [] [Type: windows::core::strings::pcwstr::PCWSTR] + [len] : 0x10 + [chars] + [0] : 0x54 'T' [Type: char16_t] + [1] : 0x68 'h' [Type: char16_t] + [2] : 0x69 'i' [Type: char16_t] + [3] : 0x73 's' [Type: char16_t] + [4] : 0x20 ' ' [Type: char16_t] + [5] : 0x69 'i' [Type: char16_t] + [6] : 0x73 's' [Type: char16_t] + [7] : 0x20 ' ' [Type: char16_t] + [8] : 0x61 'a' [Type: char16_t] + [9] : 0x20 ' ' [Type: char16_t] + [10] : 0x50 'P' [Type: char16_t] + [11] : 0x43 'C' [Type: char16_t] + [12] : 0x57 'W' [Type: char16_t] + [13] : 0x53 'S' [Type: char16_t] + [14] : 0x54 'T' [Type: char16_t] + [15] : 0x52 'R' [Type: char16_t] + +empty : "" [Type: windows::core::strings::hstring::HSTRING] + [] [Type: windows::core::strings::hstring::HSTRING] + [len] : 0x0 [Type: unsigned int] + +hstring : "This is an HSTRING" [Type: windows::core::strings::hstring::HSTRING] + [] [Type: windows::core::strings::hstring::HSTRING] + [len] : 0x12 [Type: unsigned int] + [ref_count] : 1 [Type: windows::core::ref_count::RefCount] + [flags] : 0x0 [Type: unsigned int] + [chars] + [0] : 0x54 'T' [Type: char16_t] + [1] : 0x68 'h' [Type: char16_t] + [2] : 0x69 'i' [Type: char16_t] + [3] : 0x73 's' [Type: char16_t] + [4] : 0x20 ' ' [Type: char16_t] + [5] : 0x69 'i' [Type: char16_t] + [6] : 0x73 's' [Type: char16_t] + [7] : 0x20 ' ' [Type: char16_t] + [8] : 0x61 'a' [Type: char16_t] + [9] : 0x6e 'n' [Type: char16_t] + [10] : 0x20 ' ' [Type: char16_t] + [11] : 0x48 'H' [Type: char16_t] + [12] : 0x53 'S' [Type: char16_t] + [13] : 0x54 'T' [Type: char16_t] + [14] : 0x52 'R' [Type: char16_t] + [15] : 0x49 'I' [Type: char16_t] + [16] : 0x4e 'N' [Type: char16_t] + [17] : 0x47 'G' [Type: char16_t] + +out_of_memory_error : 0x8007000e (Not enough memory resources are available to complete this operation.) [Type: windows::core::error::Error] + [] [Type: windows::core::error::Error] + [info] : Some [Type: enum2$ >] + +invalid_argument_error : 0x80070057 (The parameter is incorrect.) [Type: windows::core::error::Error] + [] [Type: windows::core::error::Error] + [info] : Some [Type: enum2$ >] + "# +)] +fn test_debugger_visualizer() { + let string = "Hello World!\0".to_string(); + let mut array = Array::::with_len(string.len()); + for (i, ch) in string.as_bytes().iter().enumerate() { + array[i] = ch.clone(); + } + + // Test debugger visualizations for PSTR + let mut pstr_string = "This is a PSTR\0".to_string(); + let pstr = PSTR::from_raw(pstr_string.as_mut_ptr()); + unsafe { + assert_eq!(&pstr_string.as_bytes()[..(pstr_string.len() - 1)], pstr.as_bytes()); + } + + // Test debugger visualizations for PCSTR + let pcstr_string = "This is a PCSTR\0".to_string(); + let pcstr = PCSTR::from_raw(pcstr_string.as_ptr()); + unsafe { + assert_eq!(&pcstr_string.as_bytes()[..(pcstr_string.len() - 1)], pcstr.as_bytes()); + } + + // Test debugger visualizations for PWSTR + let mut pwstr_string: Vec = vec![84, 104, 105, 115, 32, 105, 115, 32, 97, 32, 80, 87, 83, 84, 82, 0]; + let pwstr = PWSTR::from_raw(pwstr_string.as_mut_ptr()); + unsafe { + assert_eq!(&pwstr_string.as_slice()[..(pwstr_string.len() - 1)], pwstr.as_wide()); + } + + // Test debugger visualizations for PCWSTR + let pcwstr_string: Vec = vec![84, 104, 105, 115, 32, 105, 115, 32, 97, 32, 80, 67, 87, 83, 84, 82, 0]; + let pcwstr = PCWSTR::from_raw(pcwstr_string.as_ptr()); + unsafe { + assert_eq!(&pcwstr_string.as_slice()[..(pcwstr_string.len() - 1)], pcwstr.as_wide()); + } + + // Test debugger visualizations for HSTRING + let empty = HSTRING::new(); + assert!(empty.is_empty()); + + let hstring = HSTRING::from("This is an HSTRING"); + assert!(!hstring.is_empty()); + assert!(hstring.len() == 18); + + let test: IErrorInfo = Test.into(); + + unsafe { + // Test debugger visualizations for Error + let result = test.GetGUID(); + let out_of_memory_error = result.unwrap_err(); + assert_eq!(out_of_memory_error.code(), E_OUTOFMEMORY); + assert_eq!(out_of_memory_error.message(), "Out of memory message"); + + let result = test.GetSource(); + let invalid_argument_error = result.unwrap_err(); + assert_eq!(invalid_argument_error.code(), E_INVALIDARG); + assert_eq!(invalid_argument_error.message(), "Invalid argument message"); + __break(); + } +} diff --git a/crates/tools/windows/src/main.rs b/crates/tools/windows/src/main.rs index 13f092f84e..4a12599901 100644 --- a/crates/tools/windows/src/main.rs +++ b/crates/tools/windows/src/main.rs @@ -116,6 +116,17 @@ interface = ["windows-interface"] } } + file.write_all( + r#" +# UNSTABLE FEATURES (requires Rust nightly) +# Enable to use the #[debugger_visualizer] attribute. +debugger_visualizer = [] + +"# + .as_bytes(), + ) + .unwrap(); + std::fs::copy("license-mit", "crates/libs/windows/license-mit").unwrap(); std::fs::copy("license-apache-2.0", "crates/libs/windows/license-apache-2.0").unwrap(); } diff --git a/crates/tools/yml/src/main.rs b/crates/tools/yml/src/main.rs index 40f85e6da4..853695dc86 100644 --- a/crates/tools/yml/src/main.rs +++ b/crates/tools/yml/src/main.rs @@ -107,6 +107,17 @@ jobs: yml.truncate(yml.len() - 3); + // Enable running the debugger_visualizer tests against nightly only + // since it requires the unstable debugger_visualizer feature. + // https://github.com/rust-lang/rust/issues/95939 + yml.push_str( + r#" + + - name: Test debugger_visualizer feature + run: cargo test --target ${{ matrix.target }} -p test_debugger_visualizer -- --test-threads=1 + if: matrix.version == 'nightly' && endsWith(matrix.target, '-msvc')"#, + ); + yml.push_str( r#" @@ -245,8 +256,7 @@ jobs: write!(&mut yml, "\n cargo clippy -p {} &&", name).unwrap(); } - yml.truncate(yml.len() - 3); - yml.push('\n'); + write!(&mut yml, "\n cargo clippy -p test_debugger_visualizer\n").unwrap(); std::fs::write(".github/workflows/build.yml", yml.as_bytes()).unwrap(); } @@ -271,7 +281,9 @@ fn crates() -> Vec { } for dir in dirs("crates/tests") { - crates.push(format!("test_{}", dir)); + if dir != "debugger_visualizer" { + crates.push(format!("test_{}", dir)); + } } for dir in dirs("crates/tools") { diff --git a/docs/debugger_visualizer.md b/docs/debugger_visualizer.md new file mode 100644 index 0000000000..23acb136c5 --- /dev/null +++ b/docs/debugger_visualizer.md @@ -0,0 +1,112 @@ +## Debugger Visualizers + +Many languages and debuggers enable developers to control how a type is +displayed in a debugger. These are called "debugger visualizations" or "debugger +views". + +The Windows debuggers (WinDbg\CDB) support defining custom debugger visualizations using +the `Natvis` framework. To use Natvis, developers write XML documents using the natvis +schema that describe how debugger types should be displayed with the `.natvis` extension. +(See: https://docs.microsoft.com/en-us/visualstudio/debugger/create-custom-views-of-native-objects?view=vs-2019) +The Natvis files provide patterns which match type names a description of how to display +those types. + +The Natvis schema can be found either online (See: https://code.visualstudio.com/docs/cpp/natvis#_schema) +or locally at `\Xml\Schemas\1033\natvis.xsd`. + +The GNU debugger (GDB) supports defining custom debugger views using Pretty Printers. +Pretty printers are written as python scripts that describe how a type should be displayed +when loaded up in GDB/LLDB. (See: https://sourceware.org/gdb/onlinedocs/gdb/Pretty-Printing.html#Pretty-Printing) +The pretty printers provide patterns, which match type names, and for matching +types, descibe how to display those types. (For writing a pretty printer, see: https://sourceware.org/gdb/onlinedocs/gdb/Writing-a-Pretty_002dPrinter.html#Writing-a-Pretty_002dPrinter). + +### Embedding Visualizers + +Through the use of the currently unstable `#[debugger_visualizer]` attribute, the `windows` +crate can embed debugger visualizers into the crate metadata. + +Currently the two types of visualizers supported are Natvis and Pretty printers. + +For Natvis files, when linking an executable with a crate that includes Natvis files, +the MSVC linker will embed the contents of all Natvis files into the generated `PDB`. + +For pretty printers, the compiler will encode the contents of the pretty printer +in the `.debug_gdb_scripts` section of the `ELF` generated. + +### Testing Visualizers + +The `windows` crate supports testing debugger visualizers defined for this crate. The entry point for +these tests are `crates/tests/debugger_visualizer`. This test crate defines a single integration test +which are defined using the `debugger_test` and `debugger_test_parser` crates. The `debugger_test` crate +is a proc macro crate which defines a single proc macro attribute, `#[debugger_test]`. For more detailed +information about this crate, see https://crates.io/crates/debugger_test. The CI pipeline for the `windows` +'crate has been updated to run the debugger visualizer tests to ensure debugger visualizers do not become +broken/stale. + +The `#[debugger_test]` proc macro attribute may only be used on test functions and will run the +function under the debugger specified by the `debugger` meta item. + +This proc macro attribute has 3 required values: + +1. The first required meta item, `debugger`, takes a string value which specifies the debugger to launch. +2. The second required meta item, `commands`, takes a string of new line (`\n`) separated list of debugger +commands to run. +3. The third required meta item, `expected_statements`, takes a string of new line (`\n`) separated list of +statements that must exist in the debugger output. Pattern matching through regular expressions is also +supported by using the `pattern:` prefix for each expected statement. + +#### Example: + +```rust +#[debugger_test( + debugger = "cdb", + commands = "command1\ncommand2\ncommand3", + expected_statements = "statement1\nstatement2\nstatement3")] +fn test() { + +} +``` + +Using a multiline string is also supported, with a single debugger command/expected statement per line: + +```rust +#[debugger_test( + debugger = "cdb", + commands = " +command1 +command2 +command3", + expected_statements = " +statement1 +pattern:statement[0-9]+ +statement3")] +fn test() { + +} +``` + +In the example above, the second expected statement uses pattern matching through a regular expression +by using the `pattern:` prefix. + +#### Testing Locally + +Currently, only Natvis visualizations have been defined for the `windows` crate via `debug_metadata/windows.natvis`, +which means the debugger visualizer tests need to be run on Windows using the `*-pc-windows-msvc` targets. +To run these tests locally, first ensure the debugging tools for Windows are installed or install them following +the steps listed here, [Debugging Tools for Windows](https://docs.microsoft.com/en-us/windows-hardware/drivers/debugger/). +Once the debugging tools have been installed, the tests can be run in the same manner as they are in the CI +pipeline. + +#### Note + +When running the debugger visualizer tests, `crates/tests/debugger_visualizer`, they need to be run consecutively +and not in parallel. This can be achieved by passing the flag `--test-threads=1` to rustc. This is due to +how the debugger tests are run. Each test marked with the `#[debugger_test]` attribute launches a debugger +and attaches it to the current test process. If tests are running in parallel, the test will try to attach +a debugger to the current process which may already have a debugger attached causing the test to fail. + +For example: + +``` +cargo test -p test_debugger_visualizer -- --test-threads=1 +```