From de988a0c870516b6dc9b922151e02f87d8ec0b32 Mon Sep 17 00:00:00 2001 From: Pwootage Date: Sat, 26 Sep 2026 11:38:02 -0600 Subject: [PATCH] Add new command 'dwarf types' Exports DWARF info as json for easier use from other tooling which needs struct definitions and field offsets --- README.md | 9 + src/cmd/dwarf.rs | 130 ++++-- src/util/dwarf/layout/collector.rs | 296 ++++++++++++++ src/util/dwarf/layout/cu_context.rs | 387 ++++++++++++++++++ src/util/dwarf/layout/mod.rs | 603 ++++++++++++++++++++++++++++ src/util/dwarf/mod.rs | 1 + 6 files changed, 1400 insertions(+), 26 deletions(-) create mode 100644 src/util/dwarf/layout/collector.rs create mode 100644 src/util/dwarf/layout/cu_context.rs create mode 100644 src/util/dwarf/layout/mod.rs diff --git a/README.md b/README.md index 043d3540..7259c0f2 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ project structure and build system that uses decomp-toolkit under the hood. - [dol apply](#dol-apply) - [dol config](#dol-config) - [dwarf dump](#dwarf-dump) + - [dwarf types](#dwarf-types) - [elf disasm](#elf-disasm) - [elf fixup](#elf-fixup) - [elf2dol](#elf2dol) @@ -301,6 +302,14 @@ $ dtk dwarf dump input.elf $ dtk dwarf dump input.elf --include-erased ``` +### dwarf types + +Dumps DWARF 1.1 information from an ELF file in JSON format. (Does **not** support DWARF 2+) + +```shell +$ dtk dwarf types input.elf -o types.json +``` + ### elf disasm Disassemble an unstripped CodeWarrior ELF file. Attempts to automatically split objects and rebuild relocations diff --git a/src/cmd/dwarf.rs b/src/cmd/dwarf.rs index 70b257f0..7b0179f2 100644 --- a/src/cmd/dwarf.rs +++ b/src/cmd/dwarf.rs @@ -20,9 +20,10 @@ use typed_path::Utf8NativePathBuf; use crate::{ util::{ dwarf::{ - AttributeKind, MemberFunctionMap, TagKind, TypedefMap, parse_producer, - preprocess_cu_tag, print::tag_type_string, process_compile_unit, process_cu_tag, - process_overlay_branch, read_debug_section, should_skip_tag, + AttributeKind, DwarfInfo, MemberFunctionMap, TagKind, TypedefMap, + layout::LayoutCollector, parse_producer, preprocess_cu_tag, print::tag_type_string, + process_compile_unit, process_cu_tag, process_overlay_branch, read_debug_section, + should_skip_tag, }, file::buf_writer, path::native_path, @@ -42,6 +43,7 @@ pub struct Args { #[argp(subcommand)] enum SubCommand { Dump(DumpArgs), + Types(TypesArgs), } #[derive(FromArgs, PartialEq, Eq, Debug)] @@ -63,12 +65,80 @@ pub struct DumpArgs { include_erased: bool, } +#[derive(FromArgs, PartialEq, Eq, Debug)] +/// Exports struct, union and enum layouts from DWARF 1.1 info as JSON. +/// Types are deduplicated across compile units; definitions that disagree +/// are reported under "conflicts" instead. +#[argp(subcommand, name = "types")] +pub struct TypesArgs { + #[argp(positional, from_str_fn(native_path))] + /// Input object. (ELF or archive) + in_file: Utf8NativePathBuf, + #[argp(option, short = 'o', from_str_fn(native_path))] + /// Output JSON file. (Default: stdout) + out: Option, +} + pub fn run(args: Args) -> Result<()> { match args.command { SubCommand::Dump(c_args) => dump(c_args), + SubCommand::Types(c_args) => types(c_args), } } +fn types(args: TypesArgs) -> Result<()> { + let mut collector = LayoutCollector::default(); + let mut file = open_file(&args.in_file, true)?; + let buf = file.map()?; + if buf.starts_with(b"!\n") { + let mut archive = ar::Archive::new(buf); + while let Some(result) = archive.next_entry() { + let mut e = match result { + Ok(e) => e, + Err(e) => bail!("Failed to read archive entry: {:?}", e), + }; + let name = String::from_utf8_lossy(e.header().identifier()).to_string(); + let mut data = vec![0u8; e.header().size() as usize]; + e.read_exact(&mut data)?; + let obj_file = object::read::File::parse(&*data)?; + let Some(debug_section) = obj_file.section_by_name(".debug") else { + log::warn!("Object '{}' missing .debug section", name); + continue; + }; + let mut info = read_object_dwarf(&obj_file, debug_section, false)?; + collector.add_info(&mut info).with_context(|| format!("Processing '{name}'"))?; + } + } else { + let obj_file = object::read::File::parse(buf)?; + let debug_section = obj_file + .section_by_name(".debug") + .ok_or_else(|| anyhow!("Failed to locate .debug section"))?; + let mut info = read_object_dwarf(&obj_file, debug_section, false)?; + collector.add_info(&mut info)?; + } + if collector.errors > 0 { + log::warn!("{} tags or types could not be processed", collector.errors); + } + let layouts = collector.finish()?; + log::info!( + "{} structs, {} unions, {} enums, {} conflicts", + layouts.structs.len(), + layouts.unions.len(), + layouts.enums.len(), + layouts.conflicts.len() + ); + if let Some(out_path) = &args.out { + let mut w = buf_writer(out_path)?; + serde_json::to_writer_pretty(&mut w, &layouts)?; + w.flush()?; + } else { + let mut w = stdout().lock(); + serde_json::to_writer_pretty(&mut w, &layouts)?; + writeln!(w)?; + } + Ok(()) +} + fn dump(args: DumpArgs) -> Result<()> { // Load syntect let theme_set: ThemeSet = @@ -147,29 +217,7 @@ fn dump_debug_section( where W: Write + ?Sized, { - let mut data = debug_section.uncompressed_data()?.into_owned(); - - // Apply relocations to data - for (addr, reloc) in debug_section.relocations() { - match reloc.flags() { - RelocationFlags::Elf { r_type: elf::R_PPC_ADDR32 | elf::R_PPC_UADDR32 } => { - let target = match reloc.target() { - RelocationTarget::Symbol(symbol_idx) => { - let symbol = obj_file.symbol_by_index(symbol_idx)?; - (symbol.address() as i64 + reloc.addend()) as u32 - } - _ => bail!("Invalid .debug relocation target"), - }; - data[addr as usize..addr as usize + 4].copy_from_slice(&target.to_be_bytes()); - } - RelocationFlags::Elf { r_type: elf::R_PPC_NONE } => {} - _ => bail!("Unhandled .debug relocation type {:?}", reloc.kind()), - } - } - - let mut reader = Cursor::new(&*data); - let mut info = - read_debug_section(&mut reader, obj_file.endianness().into(), args.include_erased)?; + let mut info = read_object_dwarf(obj_file, debug_section, args.include_erased)?; for (&addr, tag) in &info.tags { log::debug!("{}: {:?}", addr, tag); @@ -328,6 +376,36 @@ where Ok(()) } +/// Reads the `.debug` section of an object, applying its relocations. +fn read_object_dwarf( + obj_file: &object::File<'_>, + debug_section: Section, + include_erased: bool, +) -> Result { + let mut data = debug_section.uncompressed_data()?.into_owned(); + + // Apply relocations to data + for (addr, reloc) in debug_section.relocations() { + match reloc.flags() { + RelocationFlags::Elf { r_type: elf::R_PPC_ADDR32 | elf::R_PPC_UADDR32 } => { + let target = match reloc.target() { + RelocationTarget::Symbol(symbol_idx) => { + let symbol = obj_file.symbol_by_index(symbol_idx)?; + (symbol.address() as i64 + reloc.addend()) as u32 + } + _ => bail!("Invalid .debug relocation target"), + }; + data[addr as usize..addr as usize + 4].copy_from_slice(&target.to_be_bytes()); + } + RelocationFlags::Elf { r_type: elf::R_PPC_NONE } => {} + _ => bail!("Unhandled .debug relocation type {:?}", reloc.kind()), + } + } + + let mut reader = Cursor::new(&*data); + read_debug_section(&mut reader, obj_file.endianness().into(), include_erased) +} + struct HighlightWriter<'a> { line: String, highlighter: Highlighter<'a>, diff --git a/src/util/dwarf/layout/collector.rs b/src/util/dwarf/layout/collector.rs new file mode 100644 index 00000000..99a22562 --- /dev/null +++ b/src/util/dwarf/layout/collector.rs @@ -0,0 +1,296 @@ +use std::collections::{BTreeMap, HashMap}; + +use anyhow::{Result, bail}; + +use super::{ + Category, Conflict, ConflictCause, ConflictVariant, Def, TypeLayouts, TypeOrigin, TypeRef, + bare_name, bare_name_with_args, cu_context::CuContext, is_qualified, render_type_ref, + strip_type_modifiers, +}; +use crate::util::dwarf::{ + DwarfInfo, MemberFunctionMap, TagKind, parse_producer, preprocess_cu_tag, process_compile_unit, +}; + +struct Variant { + def: Def, + /// Indices into [`LayoutCollector::units`]. + units: Vec, + origins: Vec, +} + +#[derive(Default)] +struct Group { + variants: Vec, + /// Forward declarations and superseded stubs; they refer to whichever variant wins. + forward_origins: Vec, +} + +/// Accumulates type definitions across compile units, deduplicating by qualified name. +#[derive(Default)] +pub struct LayoutCollector { + groups: BTreeMap<(Category, String), Group>, + units: Vec, + pub errors: usize, +} + +impl LayoutCollector { + fn add(&mut self, name: String, def: Def, origin: TypeOrigin) { + let group = self.groups.entry((def.category(), name)).or_default(); + if !def.is_complete() + || (!def.has_members() && group.variants.iter().any(|v| v.def.has_members())) + { + group.forward_origins.push(origin); + return; + } + if def.has_members() { + let (stubs, rest) = + group.variants.drain(..).partition::, _>(|v| !v.def.has_members()); + group.variants = rest; + group.forward_origins.extend(stubs.into_iter().flat_map(|v| v.origins)); + } + add_variant(&mut group.variants, def, origin); + } + + /// Collects all struct, union and enum definitions from every compile unit in `info`. + pub fn add_info(&mut self, info: &mut DwarfInfo) -> Result<()> { + let Some((_, mut tag)) = info.tags.first_key_value() else { + return Ok(()); + }; + loop { + match tag.kind { + TagKind::Padding | TagKind::MwOverlayBranch => {} + TagKind::CompileUnit => { + let unit = process_compile_unit(tag)?; + if let Some(producer) = &unit.producer { + info.producer = parse_producer(producer); + } + let unit_index = self.units.len() as u32; + self.units.push(unit.name); + let children = tag.children(&info.tags); + info.member_functions.replace(MemberFunctionMap::new()); + for &child in &children { + preprocess_cu_tag(info, child); + } + let mut cx = CuContext::new(info, unit_index, &children); + let mut defs = Vec::new(); + for &child in &children { + cx.collect_tag(child, &mut defs); + } + self.errors += cx.errors; + for (name, def, key) in defs { + self.add(name, def, (unit_index, key)); + } + } + kind => bail!("Unhandled root tag type {:?}", kind), + } + match tag.next_sibling(&info.tags) { + Some(next) => tag = next, + None => break, + } + } + Ok(()) + } + + /// Merges bare-named definitions into matching qualified ones, then resolves every type + /// reference to its final name. + pub fn finish(mut self) -> Result { + let mut causes = HashMap::<(Category, &str), ConflictCause>::new(); + for (category, name) in self.groups.keys() { + if is_qualified(name) { + let cause = if bare_name_with_args(name).contains('<') { + ConflictCause::UnresolvedTemplate + } else { + ConflictCause::UnresolvedScope + }; + let entry = causes.entry((*category, bare_name(name))).or_insert(cause); + if cause == ConflictCause::UnresolvedTemplate { + *entry = cause; + } + } + } + let causes: HashMap<(Category, String), ConflictCause> = + causes.into_iter().map(|((c, n), cause)| ((c, n.to_string()), cause)).collect(); + self.merge_bare_names(); + + let mut resolved = HashMap::)>::new(); + for ((_, name), group) in &self.groups { + let conflicted = group.variants.len() > 1; + for (i, v) in group.variants.iter().enumerate() { + for &origin in &v.origins { + resolved.insert(origin, (name.clone(), conflicted.then_some(i))); + } + } + for &origin in &group.forward_origins { + resolved.insert(origin, (name.clone(), None)); + } + } + let mut resolve = |t: &mut TypeRef| { + if let Some((name, variant)) = t.origin.and_then(|o| resolved.get(&o)) { + t.base.clone_from(name); + t.variant = *variant; + } + if !t.is_opaque() { + t.display = render_type_ref(t); + } + }; + + let mut out = + TypeLayouts { structs: vec![], unions: vec![], enums: vec![], conflicts: vec![] }; + for ((category, name), group) in self.groups { + let mut variants = group.variants; + for v in &mut variants { + v.def.set_name(name.clone()); + v.def.for_each_type_mut(&mut resolve); + if let Def::Struct(s) = &mut v.def { + for base in &mut s.bases { + if let Some((name, variant)) = base.origin.and_then(|o| resolved.get(&o)) { + base.name.clone_from(name); + base.variant = *variant; + } + } + } + } + if variants.len() > 1 { + log::warn!( + "Conflicting definitions of {} {} ({} variants)", + category.as_str(), + name, + variants.len() + ); + let likely_cause = match causes.get(&(category, name.clone())) { + Some(&cause) => cause, + None if variants.windows(2).all(|w| w[0].def.same_shape(&w[1].def)) => { + ConflictCause::UnresolvedTemplate + } + None => ConflictCause::Unknown, + }; + out.conflicts.push(Conflict { + name, + category: category.as_str(), + likely_cause, + variants: variants + .into_iter() + .map(|v| { + let mut units: Vec = + v.units.iter().map(|&u| self.units[u as usize].clone()).collect(); + units.sort(); + units.dedup(); + Ok(ConflictVariant { + compile_units: units, + definition: v.def.to_json()?, + }) + }) + .collect::>()?, + }); + continue; + } + match variants.pop().map(|v| v.def) { + Some(Def::Struct(s)) => out.structs.push(s), + Some(Def::Union(u)) => out.unions.push(u), + Some(Def::Enum(e)) => out.enums.push(e), + None => {} + } + } + Ok(out) + } + + /// Moves each variant of a bare-named group (`vector`) into the qualified group + /// (`rstl::vector`) whose layout it matches, when exactly one does and the layout + /// determines the template arguments. + fn merge_bare_names(&mut self) { + let mut qualified_by_bare = BTreeMap::<(Category, String), Vec>::new(); + for (category, name) in self.groups.keys() { + if is_qualified(name) { + qualified_by_bare + .entry((*category, bare_name(name).to_string())) + .or_default() + .push(name.clone()); + } + } + for ((category, bare), candidates) in qualified_by_bare { + let Some(mut group) = self.groups.remove(&(category, bare.clone())) else { + continue; + }; + let mut unmatched = vec![]; + for variant in group.variants { + let mut matches = candidates.iter().flat_map(|name| { + let target = &self.groups[&(category, name.clone())]; + target + .variants + .iter() + .enumerate() + .filter(|(_, v)| v.def.same_layout(&variant.def)) + .filter(|_| template_args_visible(name, &variant.def)) + .map(move |(i, _)| (name, i)) + }); + match (matches.next(), matches.next()) { + (Some((name, i)), None) => { + let target = &mut self.groups.get_mut(&(category, name.clone())).unwrap(); + target.variants[i].units.extend(variant.units); + target.variants[i].origins.extend(variant.origins); + } + _ => unmatched.push(variant), + } + } + group.variants = unmatched; + if !group.variants.is_empty() || !group.forward_origins.is_empty() { + self.groups.insert((category, bare), group); + } + } + } +} + +/// Whether every template argument in `name` names a type used by `def`'s members or bases. +/// Otherwise a matching layout proves nothing about the arguments: `rstl::reserved_vector` +/// stores `unsigned char mData[8 * sizeof(T)]`, so every 8-byte `T` has the same layout. +fn template_args_visible(name: &str, def: &Def) -> bool { + let mut used = std::collections::HashSet::new(); + let mut def = def.clone(); + def.for_each_type_mut(&mut |t| { + used.insert(bare_name(&t.base).to_string()); + }); + if let Def::Struct(s) = &def { + used.extend(s.bases.iter().map(|b| bare_name(&b.name).to_string())); + } + template_args(name) + .into_iter() + .all(|arg| strip_type_modifiers(arg).is_some_and(|t| used.contains(bare_name(t)))) +} + +/// All template arguments of all components of a qualified name. +pub(super) fn template_args(name: &str) -> Vec<&str> { + let mut out = vec![]; + let mut depth = 0i32; + let mut start = 0; + for (i, c) in name.char_indices() { + match c { + '<' => { + depth += 1; + if depth == 1 { + start = i + 1; + } + } + '>' => { + if depth == 1 { + out.push(name[start..i].trim()); + } + depth -= 1; + } + ',' if depth == 1 => { + out.push(name[start..i].trim()); + start = i + 1; + } + _ => {} + } + } + out +} + +fn add_variant(variants: &mut Vec, def: Def, origin: TypeOrigin) { + if let Some(v) = variants.iter_mut().find(|v| v.def.same_layout(&def)) { + v.units.push(origin.0); + v.origins.push(origin); + } else { + variants.push(Variant { def, units: vec![origin.0], origins: vec![origin] }); + } +} diff --git a/src/util/dwarf/layout/cu_context.rs b/src/util/dwarf/layout/cu_context.rs new file mode 100644 index 00000000..94d432fc --- /dev/null +++ b/src/util/dwarf/layout/cu_context.rs @@ -0,0 +1,387 @@ +use std::collections::HashMap; + +use anyhow::Result; +use cwdemangle::demangle as cw_demangle; + +use super::{ + BaseKind, BaseLayout, BitLayout, Def, EnumLayout, EnumMemberLayout, InlineDef, MemberLayout, + ModifierKind, StructLayout, TypeRef, UnionLayout, bare_name, demangled_parameters, + demangled_scope, strip_type_modifiers, +}; +use crate::util::dwarf::{ + AttributeKind, DwarfInfo, EnumerationType, Modifier, Producer, StructureKind, StructureMember, + StructureType, Tag, TagKind, Type, TypeKind, TypedefMap, UnionType, UserDefinedType, + get_udt_by_key, print::type_string, process_type, ud_type, +}; + +/// Anonymous types nested deeper than this are not expanded inline. +const MAX_INLINE_DEPTH: usize = 8; + +/// All the information from a single compilation unit. +pub(super) struct CuContext<'a> { + info: &'a DwarfInfo, + unit: u32, + /// Qualified names recovered from mangled names, keyed by UDT tag. + recovered: HashMap, + typedef_names: HashMap, + /// Enclosing UDT of nested types that appear as tag children (GCC). + tree_parents: HashMap, + names: HashMap>, + empty_typedefs: TypedefMap, + pub(super) errors: usize, +} + +impl<'a> CuContext<'a> { + pub(super) fn new(info: &'a DwarfInfo, unit: u32, children: &[&'a Tag]) -> Self { + let mut cx = Self { + info, + unit, + recovered: HashMap::new(), + typedef_names: HashMap::new(), + tree_parents: HashMap::new(), + names: HashMap::new(), + empty_typedefs: TypedefMap::new(), + errors: 0, + }; + let member_functions = info.member_functions.borrow(); + for (&udt, functions) in member_functions.iter() { + for function in functions { + if let Some(mangled) = info.tags.get(function).and_then(|t| cx.mangled_name(t)) { + cx.recover_scope(udt, &mangled); + } + } + } + for &child in children { + match child.kind { + TagKind::Typedef => { + if let (Some(name), Some(key)) = ( + child.string_attribute(AttributeKind::Name), + child.reference_attribute(AttributeKind::UserDefType), + ) { + cx.typedef_names.entry(key).or_insert_with(|| name.clone()); + } + } + TagKind::StructureType | TagKind::ClassType | TagKind::UnionType => { + cx.index_udt_children(child); + } + TagKind::GlobalSubroutine | TagKind::Subroutine => cx.recover_parameters(child), + _ => {} + } + if let (Some(parent), Some(mangled)) = + (child.reference_attribute(AttributeKind::Member), cx.mangled_name(child)) + { + cx.recover_scope(parent, &mangled); + } + } + cx + } + + fn index_udt_children(&mut self, tag: &Tag) { + for child in tag.children(&self.info.tags) { + match child.kind { + TagKind::StructureType + | TagKind::ClassType + | TagKind::UnionType + | TagKind::EnumerationType => { + self.tree_parents.insert(child.key, tag.key); + self.index_udt_children(child); + } + TagKind::GlobalVariable | TagKind::Typedef => { + if let Some(mangled) = self.mangled_name(child) { + self.recover_scope(tag.key, &mangled); + } + } + _ => {} + } + } + } + + fn mangled_name(&self, tag: &Tag) -> Option { + if let Some(name) = tag.string_attribute(AttributeKind::MwMangled) { + return Some(name.clone()); + } + let spec = tag.reference_attribute(AttributeKind::Specification)?; + self.info.tags.get(&spec)?.string_attribute(AttributeKind::MwMangled).cloned() + } + + /// Records `name` as the qualified name of UDT `key` if their bare names agree. + fn recover(&mut self, key: u32, name: &str) { + let Some(tag) = self.info.tags.get(&key) else { return }; + if !matches!( + tag.kind, + TagKind::StructureType + | TagKind::ClassType + | TagKind::UnionType + | TagKind::EnumerationType + ) { + return; + } + if tag.string_attribute(AttributeKind::Name).is_some_and(|n| n == bare_name(name)) { + self.recovered.entry(key).or_insert_with(|| name.to_string()); + } + } + + fn recover_scope(&mut self, key: u32, mangled: &str) { + if self.info.producer != Producer::MWCC { + return; + } + if let Some(scope) = + cw_demangle(mangled, &Default::default()).and_then(|d| demangled_scope(&d)) + { + self.recover(key, &scope); + } + } + + /// Pairs a function's DWARF parameter types with the parameter types in its demangled + /// name, recovering qualified names for any UDT passed by value, pointer or reference. + fn recover_parameters(&mut self, tag: &Tag) { + if self.info.producer != Producer::MWCC { + return; + } + let Some(demangled) = + self.mangled_name(tag).and_then(|m| cw_demangle(&m, &Default::default())) + else { + return; + }; + if let Some(scope) = demangled_scope(&demangled) { + // Static member functions have no `this` parameter to link them to their class + if let Some(key) = tag.reference_attribute(AttributeKind::Member) { + self.recover(key, &scope); + } + } + let Some(names) = demangled_parameters(&demangled) else { return }; + let mut keys = vec![]; + for child in tag.children(&self.info.tags) { + if child.kind != TagKind::FormalParameter { + continue; + } + if child.string_attribute(AttributeKind::Name).is_some_and(|n| n == "this") { + continue; + } + match child.type_attribute().and_then(|a| process_type(a, self.info.e).ok()) { + Some(Type { kind: TypeKind::UserDefined(key), .. }) => keys.push(Some(key)), + Some(_) => keys.push(None), + None => return, + } + } + if keys.len() != names.len() { + return; + } + for (key, name) in keys.into_iter().zip(names) { + if let (Some(key), Some(name)) = (key, strip_type_modifiers(name)) { + self.recover(key, name); + } + } + } + + pub(super) fn collect_tag(&mut self, tag: &Tag, out: &mut Vec<(String, Def, u32)>) { + if !matches!( + tag.kind, + TagKind::StructureType + | TagKind::ClassType + | TagKind::UnionType + | TagKind::EnumerationType + ) { + return; + } + let udt = match ud_type(self.info, tag) { + Ok(udt) => udt, + Err(e) => { + log::debug!("Failed to process tag {:X}: {:#}", tag.key, e); + self.errors += 1; + return; + } + }; + if matches!(udt, UserDefinedType::Structure(_)) { + // Nested types of GCC structs are tag children rather than top-level tags + for child in tag.children(&self.info.tags) { + self.collect_tag(child, out); + } + } + let Some(name) = self.qualified_name(tag.key, 0) else { + return; + }; + let def = match &udt { + UserDefinedType::Structure(s) => Def::Struct(self.struct_layout(s, 0)), + UserDefinedType::Union(u) => Def::Union(self.union_layout(u, 0)), + UserDefinedType::Enumeration(e) => Def::Enum(enum_layout(e)), + _ => return, + }; + out.push((name, def, tag.key)); + } + + /// The best name for UDT `key` known to this compile unit; `None` for anonymous types. + fn qualified_name(&mut self, key: u32, depth: usize) -> Option { + if let Some(name) = self.names.get(&key) { + return name.clone(); + } + let name = self.compute_qualified_name(key, depth); + self.names.insert(key, name.clone()); + name + } + + fn compute_qualified_name(&mut self, key: u32, depth: usize) -> Option { + if let Some(name) = self.recovered.get(&key) { + return Some(name.clone()); + } + let tag = self.info.tags.get(&key)?; + let bare = match tag.string_attribute(AttributeKind::Name) { + Some(name) if !name.starts_with('@') => name.clone(), + _ => return self.typedef_names.get(&key).cloned(), + }; + let parent = tag + .reference_attribute(AttributeKind::Member) + .or_else(|| self.tree_parents.get(&key).copied()); + if let Some(parent) = parent.filter(|_| depth < 16) { + if let Some(parent_name) = self.qualified_name(parent, depth + 1) { + return Some(format!("{parent_name}::{bare}")); + } + } + Some(bare) + } + + fn struct_layout(&mut self, t: &StructureType, depth: usize) -> StructLayout { + let bases = t + .bases + .iter() + .map(|b| { + let (name, origin) = match b.base_type.kind { + TypeKind::UserDefined(key) => { + (self.qualified_name(key, 0), Some((self.unit, key))) + } + TypeKind::Fundamental(_) => (None, None), + }; + BaseLayout { + name: name.or_else(|| b.name.clone()).unwrap_or_default(), + variant: None, + offset: b.offset, + virtual_base: b.virtual_base, + origin, + } + }) + .collect(); + StructLayout { + name: None, + kind: match t.kind { + StructureKind::Struct => "struct", + StructureKind::Class => "class", + }, + byte_size: t.byte_size.unwrap_or(0), + bases, + members: self.members(&t.members, depth), + } + } + + fn union_layout(&mut self, t: &UnionType, depth: usize) -> UnionLayout { + UnionLayout { name: None, byte_size: t.byte_size, members: self.members(&t.members, depth) } + } + + fn members(&mut self, members: &[StructureMember], depth: usize) -> Vec { + members + .iter() + .map(|m| MemberLayout { + name: m.name.clone(), + offset: m.offset, + byte_size: m.byte_size.or_else(|| m.kind.size(self.info).ok()), + bit: m + .bit + .as_ref() + .map(|b| BitLayout { bit_size: b.bit_size, bit_offset: b.bit_offset }), + kind: self.type_ref(&m.kind, depth), + }) + .collect() + } + + fn type_ref(&mut self, t: &Type, depth: usize) -> TypeRef { + self.type_ref_inner(t, depth).unwrap_or_else(|e| { + log::debug!("Failed to resolve type {:?}: {:#}", t, e); + self.errors += 1; + TypeRef::new(BaseKind::Unknown) + }) + } + + fn type_ref_inner(&mut self, t: &Type, depth: usize) -> Result { + let modifiers: Vec = + t.modifiers.iter().rev().map(|&m| modifier_kind(m)).collect(); + let key = match t.kind { + TypeKind::Fundamental(ft) => { + let mut out = TypeRef::new(BaseKind::Fundamental); + out.base = ft.name()?.to_string(); + out.modifiers = modifiers; + return Ok(out); + } + TypeKind::UserDefined(key) => key, + }; + let udt = get_udt_by_key(self.info, key)?; + let mut out = match &udt { + UserDefinedType::Array(a) => { + let element = self.type_ref(&a.element_type, depth); + if !element.array_modifiers.is_empty() { + return self.opaque_type(t, BaseKind::Unknown); + } + let mut dims: Vec> = + a.dimensions.iter().map(|d| d.size.map(|s| s.get())).collect(); + // MWCC lists dimensions innermost first + if self.info.producer == Producer::MWCC { + dims.reverse(); + } + dims.extend(element.array_dims); + return Ok(TypeRef { array_dims: dims, array_modifiers: modifiers, ..element }); + } + UserDefinedType::Subroutine(_) => return self.opaque_type(t, BaseKind::Function), + UserDefinedType::PtrToMember(_) => return self.opaque_type(t, BaseKind::PtrToMember), + UserDefinedType::Structure(s) => TypeRef::new(match s.kind { + StructureKind::Struct => BaseKind::Struct, + StructureKind::Class => BaseKind::Class, + }), + UserDefinedType::Union(_) => TypeRef::new(BaseKind::Union), + UserDefinedType::Enumeration(_) => TypeRef::new(BaseKind::Enum), + }; + out.modifiers = modifiers; + match self.qualified_name(key, 0) { + Some(name) => { + out.base = name; + out.origin = Some((self.unit, key)); + } + None if depth < MAX_INLINE_DEPTH => { + out.inline = Some(Box::new(match &udt { + UserDefinedType::Structure(s) => { + InlineDef::Struct(self.struct_layout(s, depth + 1)) + } + UserDefinedType::Union(u) => InlineDef::Union(self.union_layout(u, depth + 1)), + UserDefinedType::Enumeration(e) => InlineDef::Enum(enum_layout(e)), + _ => unreachable!(), + })); + } + None => {} + } + Ok(out) + } + + fn opaque_type(&self, t: &Type, base_kind: BaseKind) -> Result { + let mut out = TypeRef::new(base_kind); + out.display = type_string(self.info, &self.empty_typedefs, t, false)?.to_string(); + Ok(out) + } +} + +fn modifier_kind(m: Modifier) -> ModifierKind { + match m { + Modifier::MwPointerTo | Modifier::PointerTo => ModifierKind::Pointer, + Modifier::ReferenceTo => ModifierKind::Reference, + Modifier::Const => ModifierKind::Const, + Modifier::Volatile => ModifierKind::Volatile, + } +} + +fn enum_layout(t: &EnumerationType) -> EnumLayout { + EnumLayout { + name: None, + byte_size: t.byte_size, + members: t + .members + .iter() + .map(|m| EnumMemberLayout { name: m.name.clone(), value: m.value }) + .collect(), + } +} diff --git a/src/util/dwarf/layout/mod.rs b/src/util/dwarf/layout/mod.rs new file mode 100644 index 00000000..93b9347a --- /dev/null +++ b/src/util/dwarf/layout/mod.rs @@ -0,0 +1,603 @@ +//! Extraction of deduplicated type layouts (structs, unions, enums) from DWARF 1.1 info, +//! for consumers that need member offsets rather than printable C++. +//! +//! MWCC's DWARF 1.1 names types by their bare identifier: `rstl::vector` is `vector` +//! and `CStateManager::EInitPhase` is `EInitPhase`, with no link to the enclosing scope. +//! Qualified names are recovered from mangled function and static member names in the same +//! compile unit. Names are then settled across all compile units in [`LayoutCollector::finish`], +//! where a bare-named definition is merged into the one qualified definition with an identical +//! layout, if there is exactly one. Anything still ambiguous is reported under `conflicts`, and +//! references to it carry the index of the variant they use. + +mod collector; +mod cu_context; + +use anyhow::Result; +pub use collector::LayoutCollector; +use serde::Serialize; + +#[derive(Serialize)] +pub struct TypeLayouts { + pub structs: Vec, + pub unions: Vec, + pub enums: Vec, + pub conflicts: Vec, +} + +#[derive(Serialize, Clone, Debug)] +pub struct StructLayout { + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + pub kind: &'static str, + pub byte_size: u32, + #[serde(skip_serializing_if = "Vec::is_empty")] + pub bases: Vec, + #[serde(skip_serializing_if = "Vec::is_empty")] + pub members: Vec, +} + +#[derive(Serialize, Clone, Debug)] +pub struct BaseLayout { + pub name: String, + /// Index into the `conflicts` entry for `name`, if it has conflicting definitions. + #[serde(skip_serializing_if = "Option::is_none")] + pub variant: Option, + pub offset: u32, + #[serde(rename = "virtual")] + pub virtual_base: bool, + #[serde(skip)] + origin: Option, +} + +#[derive(Serialize, Clone, Debug)] +pub struct MemberLayout { + pub name: Option, + pub offset: u32, + pub byte_size: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub bit: Option, + #[serde(rename = "type")] + pub kind: TypeRef, +} + +/// DWARF bitfield semantics: `bit_offset` counts from the most significant bit of the +/// member's `byte_size`-sized storage unit at `offset`. +#[derive(Serialize, Clone, Debug, PartialEq, Eq)] +pub struct BitLayout { + pub bit_size: u32, + pub bit_offset: u16, +} + +#[derive(Serialize, Clone, Debug)] +pub struct UnionLayout { + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + pub byte_size: u32, + pub members: Vec, +} + +#[derive(Serialize, Clone, Debug, PartialEq, Eq)] +pub struct EnumLayout { + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + pub byte_size: u32, + pub members: Vec, +} + +#[derive(Serialize, Clone, Debug, PartialEq, Eq)] +pub struct EnumMemberLayout { + pub name: String, + pub value: i32, +} + +#[derive(Serialize, Clone, Copy, Debug, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum BaseKind { + Fundamental, + Struct, + Class, + Union, + Enum, + Function, + PtrToMember, + Unknown, +} + +#[derive(Serialize, Clone, Copy, Debug, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum ModifierKind { + Pointer, + Reference, + Const, + Volatile, +} + +/// A member type, flattened to `base`, then `modifiers`, then `array_dims`. +/// E.g. `const CActor* x[4]` is base `CActor`, modifiers `[const, pointer]`, dims `[4]`. +#[derive(Serialize, Clone, Debug)] +pub struct TypeRef { + pub display: String, + /// Fundamental type name or qualified UDT name. Empty for anonymous types, + /// function types and pointers to members. + pub base: String, + pub base_kind: BaseKind, + /// Index into the `conflicts` entry for `base`, if it has conflicting definitions. + #[serde(skip_serializing_if = "Option::is_none")] + pub variant: Option, + /// Applied to `base`, innermost first (C reading order). + #[serde(skip_serializing_if = "Vec::is_empty")] + pub modifiers: Vec, + /// Outermost first, as written in C. `null` for an unsized dimension. + #[serde(skip_serializing_if = "Vec::is_empty")] + pub array_dims: Vec>, + /// Modifiers applied to the array as a whole, e.g. `pointer` for `float (*)[3]`. + #[serde(skip_serializing_if = "Vec::is_empty")] + pub array_modifiers: Vec, + /// Definition of an anonymous struct/union/enum base type. + #[serde(skip_serializing_if = "Option::is_none")] + pub inline: Option>, + #[serde(skip)] + origin: Option, +} + +impl TypeRef { + fn new(base_kind: BaseKind) -> Self { + Self { + display: String::new(), + base: String::new(), + base_kind, + variant: None, + modifiers: vec![], + array_dims: vec![], + array_modifiers: vec![], + inline: None, + origin: None, + } + } + + /// Function types, pointers to members and types that don't fit the flattened form only + /// have a display string, taken from the C++ printer. + fn is_opaque(&self) -> bool { + matches!(self.base_kind, BaseKind::Function | BaseKind::PtrToMember | BaseKind::Unknown) + } +} + +#[derive(Serialize, Clone, Debug)] +#[serde(untagged)] +pub enum InlineDef { + Struct(StructLayout), + Union(UnionLayout), + Enum(EnumLayout), +} + +#[derive(Serialize)] +pub struct Conflict { + pub name: String, + pub category: &'static str, + pub likely_cause: ConflictCause, + pub variants: Vec, +} + +#[derive(Serialize, Clone, Copy, Debug, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum ConflictCause { + /// Instantiations of a template whose arguments could not be recovered: either other + /// instantiations were qualified, or all variants share member names and offsets and + /// differ only in member types. + UnresolvedTemplate, + /// Other types with this bare name are nested in a class or namespace; these are likely + /// distinct nested types whose scope could not be recovered. + UnresolvedScope, + /// No qualified type has this bare name: a possible ODR violation, or distinct nested + /// types whose scope was never recovered. + Unknown, +} + +#[derive(Serialize)] +pub struct ConflictVariant { + pub compile_units: Vec, + pub definition: serde_json::Value, +} + +/// A UDT tag within a specific compile unit: (compile unit index, tag key). +type TypeOrigin = (u32, u32); + +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] +enum Category { + Struct, + Union, + Enum, +} + +impl Category { + fn as_str(self) -> &'static str { + match self { + Category::Struct => "struct", + Category::Union => "union", + Category::Enum => "enum", + } + } +} + +#[derive(Clone, Debug)] +enum Def { + Struct(StructLayout), + Union(UnionLayout), + Enum(EnumLayout), +} + +impl Def { + fn category(&self) -> Category { + match self { + Def::Struct(_) => Category::Struct, + Def::Union(_) => Category::Union, + Def::Enum(_) => Category::Enum, + } + } + + /// Forward declarations have no size and no members. + fn is_complete(&self) -> bool { + match self { + Def::Struct(s) => s.byte_size != 0 || !s.members.is_empty() || !s.bases.is_empty(), + Def::Union(u) => u.byte_size != 0 || !u.members.is_empty(), + Def::Enum(e) => e.byte_size != 0 || !e.members.is_empty(), + } + } + + /// MWCC sometimes emits a class with its bases and size but no members (e.g. in + /// `TCastTo.cpp`); such a definition is superseded by one that has members. + fn has_members(&self) -> bool { + match self { + Def::Struct(s) => !s.members.is_empty(), + Def::Union(u) => !u.members.is_empty(), + Def::Enum(_) => true, + } + } + + /// Layout equality. Ignores struct vs. class, and compares type names without scopes or + /// template arguments, since compile units differ in how much of a name they can recover. + fn same_layout(&self, other: &Def) -> bool { + match (self, other) { + (Def::Struct(a), Def::Struct(b)) => same_struct(a, b), + (Def::Union(a), Def::Union(b)) => same_union(a, b), + (Def::Enum(a), Def::Enum(b)) => same_enum(a, b), + _ => false, + } + } + + /// Like [`Def::same_layout`], but ignoring member types. + fn same_shape(&self, other: &Def) -> bool { + fn members(a: &[MemberLayout], b: &[MemberLayout]) -> bool { + a.len() == b.len() + && a.iter().zip(b).all(|(a, b)| { + a.name == b.name + && a.offset == b.offset + && a.byte_size == b.byte_size + && a.bit == b.bit + }) + } + match (self, other) { + (Def::Struct(a), Def::Struct(b)) => { + a.byte_size == b.byte_size + && a.bases.len() == b.bases.len() + && a.bases.iter().zip(&b.bases).all(|(a, b)| a.offset == b.offset) + && members(&a.members, &b.members) + } + (Def::Union(a), Def::Union(b)) => { + a.byte_size == b.byte_size && members(&a.members, &b.members) + } + _ => false, + } + } + + fn for_each_type_mut(&mut self, f: &mut impl FnMut(&mut TypeRef)) { + match self { + Def::Struct(s) => struct_types_mut(s, f), + Def::Union(u) => members_types_mut(&mut u.members, f), + Def::Enum(_) => {} + } + } + + fn set_name(&mut self, name: String) { + match self { + Def::Struct(s) => s.name = Some(name), + Def::Union(u) => u.name = Some(name), + Def::Enum(e) => e.name = Some(name), + } + } + + fn to_json(&self) -> Result { + Ok(match self { + Def::Struct(s) => serde_json::to_value(s)?, + Def::Union(u) => serde_json::to_value(u)?, + Def::Enum(e) => serde_json::to_value(e)?, + }) + } +} + +fn struct_types_mut(s: &mut StructLayout, f: &mut impl FnMut(&mut TypeRef)) { + members_types_mut(&mut s.members, f); +} + +fn members_types_mut(members: &mut [MemberLayout], f: &mut impl FnMut(&mut TypeRef)) { + for m in members { + match m.kind.inline.as_deref_mut() { + Some(InlineDef::Struct(s)) => struct_types_mut(s, f), + Some(InlineDef::Union(u)) => members_types_mut(&mut u.members, f), + _ => {} + } + f(&mut m.kind); + } +} + +fn same_name(a: &str, b: &str) -> bool { a == b || bare_name(a) == bare_name(b) } + +/// Strips scopes and template arguments: `rstl::vector::iterator` -> `iterator`. +fn bare_name(s: &str) -> &str { + let last = bare_name_with_args(s); + last.split('<').next().unwrap_or(last) +} + +/// Strips scopes only: `rstl::vector` -> `vector`. +fn bare_name_with_args(s: &str) -> &str { rsplit_top_level(s).map_or(s, |(_, last)| last) } + +fn is_qualified(s: &str) -> bool { s.contains("::") || s.contains('<') } + +fn same_struct(a: &StructLayout, b: &StructLayout) -> bool { + a.byte_size == b.byte_size + && a.bases.len() == b.bases.len() + && a.bases.iter().zip(&b.bases).all(|(a, b)| { + a.offset == b.offset && a.virtual_base == b.virtual_base && same_name(&a.name, &b.name) + }) + && same_members(&a.members, &b.members) +} + +fn same_union(a: &UnionLayout, b: &UnionLayout) -> bool { + a.byte_size == b.byte_size && same_members(&a.members, &b.members) +} + +fn same_enum(a: &EnumLayout, b: &EnumLayout) -> bool { + a.byte_size == b.byte_size && a.members == b.members +} + +fn same_members(a: &[MemberLayout], b: &[MemberLayout]) -> bool { + a.len() == b.len() + && a.iter().zip(b).all(|(a, b)| { + a.name == b.name + && a.offset == b.offset + && a.byte_size == b.byte_size + && a.bit == b.bit + && same_type(&a.kind, &b.kind) + }) +} + +fn same_type(a: &TypeRef, b: &TypeRef) -> bool { + use BaseKind::*; + let kind_eq = a.base_kind == b.base_kind + || matches!((a.base_kind, b.base_kind), (Struct, Class) | (Class, Struct)); + let base_eq = match (a.inline.as_deref(), b.inline.as_deref()) { + (None, None) if a.is_opaque() => a.display == b.display, + (None, None) => same_name(&a.base, &b.base), + (Some(InlineDef::Struct(a)), Some(InlineDef::Struct(b))) => same_struct(a, b), + (Some(InlineDef::Union(a)), Some(InlineDef::Union(b))) => same_union(a, b), + (Some(InlineDef::Enum(a)), Some(InlineDef::Enum(b))) => same_enum(a, b), + // C units see `typedef enum {...} X` as anonymous, C++ units name it `X` + (Some(_), None) | (None, Some(_)) => true, + _ => false, + }; + kind_eq + && base_eq + && a.modifiers == b.modifiers + && a.array_dims == b.array_dims + && a.array_modifiers == b.array_modifiers +} + +fn render_type_ref(t: &TypeRef) -> String { + let base = if !t.base.is_empty() { + t.base.clone() + } else { + match t.base_kind { + BaseKind::Struct => "struct {...}", + BaseKind::Class => "class {...}", + BaseKind::Union => "union {...}", + BaseKind::Enum => "enum {...}", + _ => "?", + } + .to_string() + }; + let mut out = apply_modifiers(base, &t.modifiers); + if !t.array_modifiers.is_empty() { + out.push_str(&format!(" ({})", apply_modifiers(String::new(), &t.array_modifiers))); + } + for dim in &t.array_dims { + match dim { + Some(n) => out.push_str(&format!("[{n}]")), + None => out.push_str("[]"), + } + } + out +} + +fn apply_modifiers(mut out: String, modifiers: &[ModifierKind]) -> String { + let mut indirect = false; + for m in modifiers { + match m { + ModifierKind::Pointer => { + out.push('*'); + indirect = true; + } + ModifierKind::Reference => { + out.push('&'); + indirect = true; + } + ModifierKind::Const | ModifierKind::Volatile => { + let word = if *m == ModifierKind::Const { "const" } else { "volatile" }; + if indirect || out.is_empty() { + out.push(' '); + out.push_str(word); + } else { + out.insert_str(0, &format!("{word} ")); + } + } + } + } + out.trim_start().to_string() +} + +/// Returns the scope of a demangled member name, e.g. `rstl::vector` for +/// `rstl::vector::push_back(const int&)`. +fn demangled_scope(demangled: &str) -> Option { + // Operator names contain `<`, `(` etc., so cut before them rather than at the last `::` + if let Some(end) = find_top_level(demangled, |s| s.starts_with("::operator")) { + return Some(demangled[..end].to_string()); + } + let end = find_top_level(demangled, |s| s.starts_with('(')).unwrap_or(demangled.len()); + let (scope, _) = rsplit_top_level(&demangled[..end])?; + Some(scope.to_string()) +} + +/// Returns the parameter types of a demangled function name. +fn demangled_parameters(demangled: &str) -> Option> { + let mut s = demangled.trim_end(); + while let Some(rest) = s.strip_suffix(" const").or_else(|| s.strip_suffix(" volatile")) { + s = rest; + } + let s = s.strip_suffix(')')?; + let mut depth = 0i32; + let mut start = None; + for (i, c) in s.char_indices().rev() { + match c { + ')' | '>' => depth += 1, + '<' => depth -= 1, + '(' if depth == 0 => { + start = Some(i + 1); + break; + } + '(' => depth -= 1, + _ => {} + } + } + let params = &s[start?..]; + if params.is_empty() || params == "void" { + return Some(vec![]); + } + let mut out = vec![]; + let mut depth = 0i32; + let mut last = 0; + for (i, c) in params.char_indices() { + match c { + '<' | '(' => depth += 1, + '>' | ')' => depth -= 1, + ',' if depth == 0 => { + out.push(params[last..i].trim()); + last = i + 1; + } + _ => {} + } + } + out.push(params[last..].trim()); + if out.last() == Some(&"...") { + out.pop(); + } + Some(out) +} + +/// `const rstl::vector&` -> `rstl::vector`. `None` for function pointers and arrays. +fn strip_type_modifiers(mut s: &str) -> Option<&str> { + loop { + let before = s; + s = s.trim(); + for suffix in ["*", "&", " const", " volatile"] { + s = s.strip_suffix(suffix).unwrap_or(s); + } + for prefix in ["const ", "volatile "] { + s = s.strip_prefix(prefix).unwrap_or(s); + } + if s == before { + break; + } + } + let outer = &s[..s.find('<').unwrap_or(s.len())]; + (!s.is_empty() && !outer.contains(['(', '[', ' '])).then_some(s) +} + +/// Splits at the last `::` outside of template arguments and parentheses. +fn rsplit_top_level(s: &str) -> Option<(&str, &str)> { + let mut depth = 0i32; + let mut split = None; + let bytes = s.as_bytes(); + for i in 0..bytes.len() { + match bytes[i] { + b'<' | b'(' => depth += 1, + b'>' | b')' => depth -= 1, + b':' if depth == 0 && bytes.get(i + 1) == Some(&b':') => { + split = Some(i); + } + _ => {} + } + } + split.map(|i| (&s[..i], &s[i + 2..])) +} + +fn find_top_level(s: &str, pred: impl Fn(&str) -> bool) -> Option { + let mut depth = 0i32; + for (i, c) in s.char_indices() { + if depth == 0 && pred(&s[i..]) { + return Some(i); + } + match c { + '<' | '(' => depth += 1, + '>' | ')' => depth -= 1, + _ => {} + } + } + None +} + +#[cfg(test)] +mod tests { + use cwdemangle::demangle as cw_demangle; + + use super::{collector::template_args, *}; + + fn demangle(s: &str) -> String { cw_demangle(s, &Default::default()).unwrap() } + + #[test] + fn scope_from_mangled() { + let d = demangle( + "push_back__Q24rstl51list<16CCameraShakeData,Q24rstl17rmemory_allocator>FRC16CCameraShakeData", + ); + assert_eq!( + demangled_scope(&d).as_deref(), + Some("rstl::list") + ); + assert_eq!(demangled_parameters(&d), Some(vec!["const CCameraShakeData&"])); + let d = demangle("__lt__9TUniqueIdCFRC9TUniqueId"); + assert_eq!(demangled_scope(&d).as_deref(), Some("TUniqueId")); + assert_eq!(demangled_parameters(&d), Some(vec!["const TUniqueId&"])); + let d = demangle( + "mNull__Q24rstl66basic_string,Q24rstl17rmemory_allocator>", + ); + assert_eq!( + demangled_scope(&d).as_deref(), + Some("rstl::basic_string, rstl::rmemory_allocator>") + ); + assert_eq!(demangled_scope(&demangle("GetFoo__Fv")), None); + assert_eq!(demangled_parameters(&demangle("GetFoo__Fv")), Some(vec![])); + } + + #[test] + fn modifiers() { + use ModifierKind::*; + assert_eq!(apply_modifiers("char".into(), &[Const, Pointer]), "const char*"); + assert_eq!(apply_modifiers("char".into(), &[Pointer, Const]), "char* const"); + assert_eq!(apply_modifiers(String::new(), &[Pointer]), "*"); + assert_eq!( + strip_type_modifiers("const rstl::vector&"), + Some("rstl::vector") + ); + assert_eq!(strip_type_modifiers("CActor* const*"), Some("CActor")); + assert_eq!(strip_type_modifiers("void (*)(int)"), None); + assert_eq!(template_args("rstl::list>::node"), vec!["A", "B", "D*"]); + } +} diff --git a/src/util/dwarf/mod.rs b/src/util/dwarf/mod.rs index e8d459df..f34c3436 100644 --- a/src/util/dwarf/mod.rs +++ b/src/util/dwarf/mod.rs @@ -1,3 +1,4 @@ +pub mod layout; pub mod print; use std::{