diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 148d4a4..28998c5 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -15,14 +15,32 @@ env: jobs: test: name: Rust - runs-on: ubuntu-latest + strategy: + matrix: + os: [ubuntu-latest, windows-latest] + + runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v2 with: fetch-depth: 0 # fetch tags for publish + # Install the nightly toolchain when testing on windows + - name: Install Rust (nightly) + if: matrix.os == 'windows-latest' + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: nightly + override: true + - run: cargo run -p xtask -- ci + if: matrix.os == 'ubuntu-latest' env: CRATES_IO_TOKEN: ${{ secrets.CRATES_IO_TOKEN }} MIRIFLAGS: -Zmiri-strict-provenance + + - name: Test with debugger_visualizer feature + run: cargo test --test debugger_visualizer --features debugger_visualizer -- --test-threads=1 + if: matrix.os == 'windows-latest' diff --git a/Cargo.toml b/Cargo.toml index b7ebb35..0fc5e8f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,8 @@ atomic-polyfill = { version = "1", optional = true } lazy_static = "1.0.0" crossbeam-utils = "0.8.7" regex = "1.2.0" +debugger_test = "0.1.0" +debugger_test_parser = "0.1.0" [features] default = ["std"] @@ -48,6 +50,10 @@ race = [] # At the moment, this feature is unused. unstable = [] +# UNSTABLE FEATURES (requires Rust nightly) +# Enable to use the #[debugger_visualizer] attribute. +debugger_visualizer = [] + parking_lot = ["parking_lot_core"] [[example]] @@ -78,5 +84,16 @@ required-features = ["std"] name = "test_synchronization" required-features = ["std"] +[[test]] +name = "debugger_visualizer" +path = "tests/debugger_visualizer.rs" +required-features = ["debugger_visualizer"] +# Do not run these tests by default. These tests need to +# be run with the additional rustc flag `--test-threads=1` +# since each test causes a debugger to attach to the current +# test process. If multiple debuggers try to attach at the same +# time, the test will fail. +test = false + [package.metadata.docs.rs] all-features = true diff --git a/debug_metadata/README.md b/debug_metadata/README.md new file mode 100644 index 0000000..d58eb98 --- /dev/null +++ b/debug_metadata/README.md @@ -0,0 +1,111 @@ +## 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 `once_cell` +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 `once_cell` crate supports testing debugger visualizers defined for this crate. The entry point for +these tests are `tests/debugger_visualizer.rs`. These tests 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 `once_cell` 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 `once_cell` crate via `debug_metadata/once_cell.natvis`, +which means the `tests/debugger_visualizer.rs` 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, `tests/debugger_visualizer.rs`, 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 --test debugger_visualizer --features debugger_visualizer -- --test-threads=1 +``` diff --git a/debug_metadata/once_cell.natvis b/debug_metadata/once_cell.natvis new file mode 100644 index 0000000..2bd6d86 --- /dev/null +++ b/debug_metadata/once_cell.natvis @@ -0,0 +1,53 @@ + + + + {value} + + queue + _marker + value + + + + + {inner.inner.v.value == 1} + + + + + inner.p + + + + + + inner + + + + + + cell + init + + + + + + __0 + + + + + + cell + init + + + + + + inner + + + \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index dcd64c9..7a2c0de 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -328,6 +328,11 @@ //! See the [tracking issue](https://github.com/rust-lang/rust/issues/74465). #![cfg_attr(not(feature = "std"), no_std)] +#![cfg_attr( + feature = "debugger_visualizer", + feature(debugger_visualizer), + debugger_visualizer(natvis_file = "../debug_metadata/once_cell.natvis") +)] #[cfg(feature = "alloc")] extern crate alloc; diff --git a/tests/debugger_visualizer.rs b/tests/debugger_visualizer.rs new file mode 100644 index 0000000..90596d4 --- /dev/null +++ b/tests/debugger_visualizer.rs @@ -0,0 +1,159 @@ +use std::num::NonZeroUsize; +use std::sync::Mutex; + +use debugger_test::debugger_test; +use once_cell::{sync, unsync}; +use once_cell::race::*; + +static VEC: sync::Lazy> = sync::Lazy::new(|| { + let mut m = Vec::with_capacity(2); + m.push("Hoyten".to_string()); + m.push("Spica".to_string()); + m +}); + +fn global_data() -> &'static Mutex> { + static INSTANCE: sync::OnceCell>> = sync::OnceCell::new(); + INSTANCE.get_or_init(|| { + let mut m = Vec::with_capacity(2); + m.push("Hoyten".to_string()); + m.push("Spica".to_string()); + Mutex::new(m) + }) +} + +#[inline(never)] +fn __break() { + println!("Breakpoint hit"); +} + +#[debugger_test( + debugger = "cdb", + commands = r#" +.nvlist +dx once_bool +dx once_non_zero_usize + +g + +dx once_bool +dx once_box +dx once_non_zero_usize + +dx debugger_visualizer::global_data::INSTANCE +dx debugger_visualizer::global_data::INSTANCE.@"[value]".__0.data + +dx debugger_visualizer::VEC + +g + +dx debugger_visualizer::VEC +dx debugger_visualizer::VEC.@"[value]".__0 + +dx lazy +dx cell +"#, + expected_statements = r#" +once_bool : false [Type: once_cell::race::OnceBool] + [] [Type: once_cell::race::OnceBool] + +once_non_zero_usize : 0x0 [Type: once_cell::race::OnceNonZeroUsize] + [] [Type: once_cell::race::OnceNonZeroUsize] + +once_bool : true [Type: once_cell::race::OnceBool] + [] [Type: once_cell::race::OnceBool] + +once_box : "Hello World" [Type: once_cell::race::once_box::OnceBox] + [] [Type: once_cell::race::once_box::OnceBox] + [len] : 0xb [Type: unsigned __int64] + [capacity] : 0xb [Type: unsigned __int64] + [chars] : "Hello World" + +once_non_zero_usize : 0x48 [Type: once_cell::race::OnceNonZeroUsize] + [] [Type: once_cell::race::OnceNonZeroUsize] + +debugger_visualizer::global_data::INSTANCE : Some [Type: once_cell::sync::OnceCell > >] + [] [Type: once_cell::sync::OnceCell > >] + [queue] [Type: core::sync::atomic::AtomicPtr] + [marker] [Type: core::marker::PhantomData >] + [value] : Some [Type: core::cell::UnsafeCell > > > >] + +debugger_visualizer::global_data::INSTANCE.@"[value]".__0.data : { len=0x2 } [Type: core::cell::UnsafeCell >] + [] [Type: core::cell::UnsafeCell >] + [len] : 0x2 [Type: unsigned __int64] + [capacity] : 0x2 [Type: unsigned __int64] + [0] : "Hoyten" [Type: alloc::string::String] + [1] : "Spica" [Type: alloc::string::String] + +debugger_visualizer::VEC : None [Type: once_cell::sync::Lazy,alloc::vec::Vec (*)()>] + [] [Type: once_cell::sync::Lazy,alloc::vec::Vec (*)()>] + [init] : Some [Type: core::cell::Cell (*)()> > >] + [queue] [Type: core::sync::atomic::AtomicPtr] + [marker] [Type: core::marker::PhantomData >] + [value] : None [Type: core::cell::UnsafeCell > > >] + +debugger_visualizer::VEC : Some [Type: once_cell::sync::Lazy,alloc::vec::Vec (*)()>] + [] [Type: once_cell::sync::Lazy,alloc::vec::Vec (*)()>] + [init] : None [Type: core::cell::Cell (*)()> > >] + [queue] [Type: core::sync::atomic::AtomicPtr] + [marker] [Type: core::marker::PhantomData >] + [value] : Some [Type: core::cell::UnsafeCell > > >] + +debugger_visualizer::VEC.@"[value]".__0 : { len=0x2 } [Type: alloc::vec::Vec] + [] [Type: alloc::vec::Vec] + [len] : 0x2 [Type: unsigned __int64] + [capacity] : 0x2 [Type: unsigned __int64] + [0] : "Hoyten" [Type: alloc::string::String] + [1] : "Spica" [Type: alloc::string::String] + +lazy : Some [Type: once_cell::unsync::Lazy] + [] [Type: once_cell::unsync::Lazy] + [init] : None [Type: core::cell::Cell > >] + [+0x004] __0 : 92 [Type: int] + +cell : Some [Type: once_cell::unsync::OnceCell] + [] [Type: once_cell::unsync::OnceCell] + [+0x000] __0 : "Hello, World!" [Type: alloc::string::String] +"# +)] +#[inline(never)] +fn test_debugger_visualizer() { + let once_bool = OnceBool::new(); + let once_box: OnceBox = OnceBox::new(); + let once_non_zero_usize = OnceNonZeroUsize::new(); + __break(); + + let _ = once_bool.get_or_init(|| { + true + }); + + let _ = once_box.get_or_init(|| { + Box::new("Hello World".to_string()) + }); + + let _ = once_non_zero_usize.get_or_init(|| { + NonZeroUsize::new(72).unwrap() + }); + + let instance = global_data(); + let map = instance.lock().unwrap(); + assert_eq!("Hoyten".to_string(), map[0]); + __break(); + + assert_eq!("Spica".to_string(), VEC[1]); + + let lazy: unsync::Lazy = unsync::Lazy::new(|| { + 92 + }); + assert_eq!(92, *lazy); + + let cell = unsync::OnceCell::new(); + assert!(cell.get().is_none()); + + let value: &String = cell.get_or_init(|| { + "Hello, World!".to_string() + }); + assert_eq!(value, "Hello, World!"); + assert!(cell.get().is_some()); + __break(); +} \ No newline at end of file