cargo_show_asm/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use opts::{Format, NameDisplay, ToDump};
4use std::{
5    array,
6    collections::{BTreeMap, BTreeSet},
7    ops::Range,
8    path::{Path, PathBuf},
9};
10
11pub mod asm;
12pub mod cached_lines;
13pub mod demangle;
14#[cfg(feature = "disasm")]
15pub mod disasm;
16pub mod llvm;
17pub mod mca;
18pub mod mir;
19pub mod opts;
20
21#[macro_export]
22macro_rules! color {
23    ($item:expr, $color:expr) => {
24        owo_colors::OwoColorize::if_supports_color(&$item, owo_colors::Stream::Stdout, $color)
25    };
26}
27
28/// Safe version of `print[ln]!` macro
29/// By default `print[ln]!` macro panics when print fails. Usually print fails when output
30/// stream is disconnected, for purposes of this application disconnected stream means output
31/// was piped somewhere and this something was terminated before printing completed.
32///
33/// At this point we might as well exit
34#[macro_export]
35macro_rules! safeprintln {
36    ($($x:expr),* $(,)?) => {{
37        use std::io::Write;
38        if writeln!(std::io::stdout(), $($x),*).is_err() {
39            std::process::exit(0);
40        }
41    }};
42}
43
44#[macro_export]
45macro_rules! safeprint {
46    ($($x:expr),* $(,)?) => {{
47        use std::io::Write;
48        if write!(std::io::stdout(), $($x),*).is_err() {
49            std::process::exit(0);
50        }
51    }};
52}
53
54#[macro_export]
55macro_rules! esafeprintln {
56    ($($x:expr),* $(,)?) => {{
57        use std::io::Write;
58        if writeln!(std::io::stderr(), $($x),*).is_err() {
59            std::process::exit(0);
60        }
61    }};
62}
63
64#[macro_export]
65macro_rules! esafeprint {
66    ($($x:expr),* $(,)?) => {{
67        use std::io::Write;
68        if write!(std::io::stderr(), $($x),*).is_err() {
69            std::process::exit(0);
70        }
71    }};
72}
73
74/// read a set of source files to a set of strings
75///
76/// perform lossy conversion to utf8
77pub fn read_sources(names: &[PathBuf]) -> anyhow::Result<Vec<String>> {
78    names
79        .iter()
80        .map(|name| {
81            let bytes = std::fs::read(name)?;
82            // For some reason llvm/rustc can produce non utf8 files...
83            // Also there's no (without unsafe) way to reuse allocation
84            // from bytes in resulting String...
85            Ok(String::from_utf8_lossy(&bytes).into_owned())
86        })
87        .collect()
88}
89
90#[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq)]
91pub struct Item {
92    // name and hashed MUST be first two fields - they are
93    // used to produce correct Ord/PartialOrd
94    /// demangled name
95    pub name: String,
96    /// demangled name with hash
97    pub hashed: String,
98    /// sequential number of demangled name
99    pub index: usize,
100    /// number of lines
101    pub len: usize,
102    /// number of non-blank lines
103    pub non_blank_len: usize,
104    /// mangled name
105    pub mangled_name: String,
106}
107
108pub fn suggest_name<'a>(
109    search: &str,
110    fmt: &Format,
111    items: impl IntoIterator<Item = &'a Item>,
112) -> ! {
113    let mut count = 0usize;
114    let names: BTreeMap<&String, Vec<usize>> =
115        items.into_iter().fold(BTreeMap::new(), |mut m, item| {
116            count += 1;
117            let entry = match fmt.name_display {
118                NameDisplay::Full => &item.hashed,
119                NameDisplay::Short => &item.name,
120                NameDisplay::Mangled => &item.mangled_name,
121            };
122            m.entry(entry).or_default().push(item.non_blank_len);
123            m
124        });
125
126    if fmt.verbosity > 0 {
127        if names.is_empty() {
128            if search.is_empty() {
129                safeprintln!(
130                    "This target defines no functions (or cargo-show-asm can't find them)"
131                );
132            } else {
133                safeprintln!("No matching functions, try relaxing your search request");
134            }
135            safeprintln!("You can pass --everything to see the demangled contents of a file");
136        } else {
137            safeprintln!("Try one of those by name or a sequence number");
138        }
139    }
140
141    #[allow(clippy::cast_sign_loss)]
142    #[allow(clippy::cast_precision_loss)]
143    let width = (count as f64).log10().ceil() as usize;
144
145    let mut ix = 0;
146    for (name, lens) in &names {
147        safeprintln!(
148            "{ix:width$} {:?} {:?}",
149            color!(name, owo_colors::OwoColorize::green),
150            color!(lens, owo_colors::OwoColorize::cyan),
151        );
152        ix += lens.len();
153    }
154
155    std::process::exit(1);
156}
157
158/// Pick an item to dump based on a goal
159///
160/// Prints suggestions and exits if goal can't be reached or more info is needed
161#[must_use]
162pub fn pick_dump_item<K: Clone>(
163    goal: ToDump,
164    fmt: &Format,
165    items: &BTreeMap<Item, K>,
166) -> Option<K> {
167    match goal {
168        // to dump everything just return an empty range
169        ToDump::Everything => None,
170
171        // By index without filtering
172        ToDump::ByIndex { value } => {
173            if let Some(range) = items.values().nth(value) {
174                Some(range.clone())
175            } else {
176                let actual = items.len();
177                esafeprintln!("You asked to display item #{value} (zero based), but there's only {actual} items");
178                std::process::exit(1);
179            }
180        }
181
182        // By index with filtering
183        ToDump::Function { function, nth } => {
184            let filtered = items
185                .iter()
186                .filter(|(item, _range)| item.name.contains(&function))
187                .collect::<Vec<_>>();
188
189            let range = if nth.is_none() && filtered.len() == 1 {
190                filtered
191                    .first()
192                    .expect("Must have one item as checked above")
193                    .1
194                    .clone()
195            } else if let Some(range) = nth.and_then(|nth| filtered.get(nth)) {
196                range.1.clone()
197            } else if let Some(value) = nth {
198                let filtered = filtered.len();
199                esafeprintln!("You asked to display item #{value} (zero based), but there's only {filtered} matching items");
200                std::process::exit(1);
201            } else {
202                if filtered.is_empty() {
203                    esafeprintln!("Can't find any items matching {function:?}");
204                } else {
205                    suggest_name(&function, fmt, filtered.iter().map(|x| x.0));
206                }
207                std::process::exit(1);
208            };
209            Some(range)
210        }
211
212        ToDump::Unspecified => {
213            let mut items_values = items.values();
214            if let [Some(item), None] = array::from_fn(|_| items_values.next()) {
215                // Automatically pick an item if only one is found
216                Some(item.clone())
217            } else {
218                // Otherwise, print suggestions and exit
219                let items = items.keys();
220                suggest_name("", fmt, items);
221            }
222        }
223    }
224}
225
226trait RawLines {
227    fn lines(&self) -> Option<&str>;
228}
229
230impl RawLines for &str {
231    fn lines(&self) -> Option<&str> {
232        Some(self)
233    }
234}
235
236/// Recursively scan for references to global objects
237fn get_context_for<R: RawLines>(
238    depth: usize,
239    all_stmts: &[R],
240    self_range: Range<usize>,
241    items: &BTreeMap<Item, Range<usize>>,
242) -> Vec<Range<usize>> {
243    let mut out = Vec::new();
244    if depth == 0 {
245        return out;
246    }
247    let items = items
248        .iter()
249        .map(|(item, range)| (item.mangled_name.as_str(), range.clone()))
250        .collect::<BTreeMap<_, _>>();
251    let mut pending = vec![(self_range.clone(), depth)];
252    let mut processed = BTreeSet::new();
253    while let Some((range, depth)) = pending.pop() {
254        for raw in all_stmts[range]
255            .iter()
256            .filter_map(R::lines)
257            .filter_map(demangle::global_reference)
258        {
259            if !processed.insert(raw) {
260                continue;
261            }
262            if let Some(range) = items.get(raw) {
263                if range == &self_range {
264                    continue;
265                }
266                if depth > 0 {
267                    pending.push((range.clone(), depth - 1));
268                }
269                out.push(range.clone());
270            }
271        }
272    }
273    out.sort_by_key(|r| r.start);
274    out
275}
276
277pub trait Dumpable {
278    type Line<'a>;
279    /// Split source code into multiple lines, code can do some parsing here
280    fn split_lines(contents: &str) -> anyhow::Result<Vec<Self::Line<'_>>>;
281
282    /// Given a set of lines find all the interesting items
283    fn find_items(lines: &[Self::Line<'_>]) -> BTreeMap<Item, Range<usize>>;
284
285    /// Initialize freshly created Dumpable using additional information from the file
286    fn init(&mut self, _lines: &[Self::Line<'_>]) {}
287
288    /// print all the lines from this range, aplying the required formatting
289    fn dump_range(&self, fmt: &Format, lines: &[Self::Line<'_>]) -> anyhow::Result<()>;
290
291    /// starting at an initial range find more ranges to include
292    fn extra_context(
293        &self,
294        fmt: &Format,
295        lines: &[Self::Line<'_>],
296        range: Range<usize>,
297        items: &BTreeMap<Item, Range<usize>>,
298    ) -> Vec<Range<usize>> {
299        #![allow(unused_variables)]
300        Vec::new()
301    }
302}
303
304/// Parse a dumpable item from a file and dump it with all the extra context
305pub fn dump_function<T: Dumpable>(
306    dumpable: &mut T,
307    goal: ToDump,
308    path: &Path,
309    fmt: &Format,
310) -> anyhow::Result<()> {
311    // first we need to read the data and do a lossy conversion to a string slice
312    // (files generated by rustc/llvm can have non-utf8 characters in them
313    let raw_bytes = std::fs::read(path)?;
314    let contents = String::from_utf8_lossy(&raw_bytes[..]);
315
316    let lines = T::split_lines(&contents)?;
317    let items = T::find_items(&lines);
318    dumpable.init(&lines);
319
320    match pick_dump_item(goal, fmt, &items) {
321        Some(range) => {
322            let context = T::extra_context(dumpable, fmt, &lines, range.clone(), &items);
323            dumpable.dump_range(fmt, &lines[range])?;
324
325            if !context.is_empty() {
326                safeprintln!(
327                    "\n======================= Additional context ========================="
328                );
329                for range in context {
330                    safeprintln!("");
331                    dumpable.dump_range(fmt, &lines[range])?;
332                }
333            }
334        }
335        None => {
336            if fmt.rust {
337                // for asm files extra_context loads rust sources
338                T::extra_context(dumpable, fmt, &lines, 0..lines.len(), &items);
339            }
340            dumpable.dump_range(fmt, &lines)?;
341        }
342    }
343    Ok(())
344}
345
346/// Mostly the same as Range, but Copy and Ord
347#[derive(Debug, Clone, Copy, Ord, PartialOrd, Eq, PartialEq)]
348pub struct URange {
349    start: usize,
350    end: usize,
351}
352
353impl From<Range<usize>> for URange {
354    fn from(Range { start, end }: Range<usize>) -> Self {
355        Self { start, end }
356    }
357}
358
359impl<T> std::ops::Index<URange> for [T] {
360    type Output = [T];
361    fn index(&self, index: URange) -> &Self::Output {
362        &self[index.start..index.end]
363    }
364}
365
366impl URange {
367    pub fn fully_contains(&self, other: Self) -> bool {
368        self.start >= other.start && self.end <= other.end
369    }
370}