From 9a4e51b155bd7e5cae8cf1e44517ef1d147abdd5 Mon Sep 17 00:00:00 2001 From: chen3feng Date: Wed, 3 Jun 2026 21:56:15 +0800 Subject: [PATCH] Add optional STL template-name simplification for undecorated names Collapses the canonical default-argument forms produced by the MSVC demangler into their conventional STL spellings, purely for display: std::basic_string,std::allocator > -> std::string std::vector > -> std::vector std::map,std::allocator<...> > -> std::map Only the default-value template forms of the standard library are matched (basic_string/_view, vector/list/deque/forward_list, set/multiset, map/multimap, unordered_*, unique_ptr); user templates and non-default arguments are left untouched. The transform is bracket-aware (walks balanced angle brackets, folds bottom-up) so nested defaults collapse correctly, and any unexpected input is returned unchanged so a displayed name can never be corrupted. Exposed as a new "Simplify STL Template Names" option on the Configuration > Appearance page (off by default). Toggling it clears the cached undecorated names and refreshes the view. The original names remain available via the existing tooltip. Co-Authored-By: Claude Opus 4.8 --- src/WinDepends/CConsts.cs | 1 + src/WinDepends/Configuration/CConfigMgr.cs | 3 + .../Forms/ConfigurationForm.Designer.cs | 17 +- src/WinDepends/Forms/ConfigurationForm.cs | 5 + src/WinDepends/MainForm/MainForm.cs | 41 +- src/WinDepends/Symbols/CStlNameSimplifier.cs | 376 ++++++++++++++++++ src/WinDepends/Symbols/CSymbolResolver.cs | 11 +- 7 files changed, 451 insertions(+), 3 deletions(-) create mode 100644 src/WinDepends/Symbols/CStlNameSimplifier.cs diff --git a/src/WinDepends/CConsts.cs b/src/WinDepends/CConsts.cs index 349104c..8df2926 100644 --- a/src/WinDepends/CConsts.cs +++ b/src/WinDepends/CConsts.cs @@ -58,6 +58,7 @@ public static class CConsts public const int TagResolveAPIsets = 123; public const int TagUpperCaseModuleNames = 124; public const int TagClearLogOnFileOpen = 125; + public const int TagSimplifyStlNames = 126; public const int TagViewExternalViewer = 200; public const int TagViewProperties = 201; public const int TagSystemInformation = 300; diff --git a/src/WinDepends/Configuration/CConfigMgr.cs b/src/WinDepends/Configuration/CConfigMgr.cs index 21c0e2b..d1ede74 100644 --- a/src/WinDepends/Configuration/CConfigMgr.cs +++ b/src/WinDepends/Configuration/CConfigMgr.cs @@ -42,6 +42,8 @@ public class CConfiguration [DataMember] public bool ViewUndecorated { get; set; } [DataMember] + public bool SimplifyStlNames { get; set; } + [DataMember] public bool ResolveAPIsets { get; set; } [DataMember] public bool FullPaths { get; set; } @@ -163,6 +165,7 @@ public CConfiguration(CConfiguration other) SortColumnModules = other.SortColumnModules; ModuleNodeDepthMax = other.ModuleNodeDepthMax; ViewUndecorated = other.ViewUndecorated; + SimplifyStlNames = other.SimplifyStlNames; ResolveAPIsets = other.ResolveAPIsets; FullPaths = other.FullPaths; AutoExpands = other.AutoExpands; diff --git a/src/WinDepends/Forms/ConfigurationForm.Designer.cs b/src/WinDepends/Forms/ConfigurationForm.Designer.cs index 49ce74c..e094836 100644 --- a/src/WinDepends/Forms/ConfigurationForm.Designer.cs +++ b/src/WinDepends/Forms/ConfigurationForm.Designer.cs @@ -59,6 +59,7 @@ private void InitializeComponent() chBoxUpperCase = new CheckBox(); chBoxResolveApiSets = new CheckBox(); chBoxUndecorateSymbols = new CheckBox(); + chBoxSimplifyStlNames = new CheckBox(); chBoxAutoExpands = new CheckBox(); groupBox2 = new GroupBox(); chBoxUseESCKey = new CheckBox(); @@ -366,6 +367,7 @@ private void InitializeComponent() groupBox3.Controls.Add(chBoxUpperCase); groupBox3.Controls.Add(chBoxResolveApiSets); groupBox3.Controls.Add(chBoxUndecorateSymbols); + groupBox3.Controls.Add(chBoxSimplifyStlNames); groupBox3.Controls.Add(chBoxAutoExpands); groupBox3.Location = new Point(6, 71); groupBox3.Name = "groupBox3"; @@ -489,7 +491,19 @@ private void InitializeComponent() chBoxUndecorateSymbols.Text = "Undecorate C++ Functions"; chBoxUndecorateSymbols.UseVisualStyleBackColor = true; chBoxUndecorateSymbols.Click += ChBox_Click; - // + // + // chBoxSimplifyStlNames + // + chBoxSimplifyStlNames.AutoSize = true; + chBoxSimplifyStlNames.Location = new Point(250, 83); + chBoxSimplifyStlNames.Name = "chBoxSimplifyStlNames"; + chBoxSimplifyStlNames.Size = new Size(200, 19); + chBoxSimplifyStlNames.TabIndex = 19; + chBoxSimplifyStlNames.Tag = "126"; + chBoxSimplifyStlNames.Text = "Simplify STL Template Names"; + chBoxSimplifyStlNames.UseVisualStyleBackColor = true; + chBoxSimplifyStlNames.Click += ChBox_Click; + // // chBoxAutoExpands // chBoxAutoExpands.AutoSize = true; @@ -1681,6 +1695,7 @@ private void InitializeComponent() private GroupBox groupBox3; private CheckBox chBoxResolveApiSets; private CheckBox chBoxUndecorateSymbols; + private CheckBox chBoxSimplifyStlNames; private CheckBox chBoxAutoExpands; private CheckBox chBoxFullPaths; private TabPage tabExternalViewer; diff --git a/src/WinDepends/Forms/ConfigurationForm.cs b/src/WinDepends/Forms/ConfigurationForm.cs index 78b26d7..0fef7ef 100644 --- a/src/WinDepends/Forms/ConfigurationForm.cs +++ b/src/WinDepends/Forms/ConfigurationForm.cs @@ -337,6 +337,7 @@ private void ConfigurationForm_Load(object sender, EventArgs e) chBoxAutoExpands.Checked = _config.AutoExpands; chBoxFullPaths.Checked = _config.FullPaths; chBoxUndecorateSymbols.Checked = _config.ViewUndecorated; + chBoxSimplifyStlNames.Checked = _config.SimplifyStlNames; chBoxResolveApiSets.Checked = _config.ResolveAPIsets; chBoxHighlightApiSet.Checked = _config.HighlightApiSet; chBoxApiSetNamespace.Checked = _config.UseApiSetSchemaFile; @@ -506,6 +507,10 @@ private void ChBox_Click(object sender, EventArgs e) _config.ViewUndecorated = isChecked; break; + case CConsts.TagSimplifyStlNames: + _config.SimplifyStlNames = isChecked; + break; + case CConsts.TagResolveAPIsets: _config.ResolveAPIsets = isChecked; break; diff --git a/src/WinDepends/MainForm/MainForm.cs b/src/WinDepends/MainForm/MainForm.cs index a5b2d6e..635ca33 100644 --- a/src/WinDepends/MainForm/MainForm.cs +++ b/src/WinDepends/MainForm/MainForm.cs @@ -198,6 +198,7 @@ public MainForm() _configuration.SymbolsStorePath = _symbolResolver.StorePath; } + _symbolResolver.SimplifyTemplateDefaults = _configuration.SimplifyStlNames; _symbolResolver.SymbolLoadStatusChanged += SymbolResolver_SymbolLoadStatusChanged; // @@ -793,6 +794,7 @@ private void ShowConfigurationForm(int pageIndex) var bFullPathPrev = _configuration.FullPaths; var bUpperCaseModulesNamesPrev = _configuration.UpperCaseModuleNames; var bUndecoratedPrev = _configuration.ViewUndecorated; + var bSimplifyStlPrev = _configuration.SimplifyStlNames; var bResolveAPISetsPrev = _configuration.ResolveAPIsets; var bHighlightAPISetsPrev = _configuration.HighlightApiSet; var bUseApiSetSchemaFilePrev = _configuration.UseApiSetSchemaFile; @@ -837,7 +839,18 @@ private void ShowConfigurationForm(int pageIndex) UpdateFileView(FileViewUpdateAction.ModulesTreeAndListChange); } - if (_configuration.ViewUndecorated != bUndecoratedPrev) + if (_configuration.SimplifyStlNames != bSimplifyStlPrev) + { + // + // The simplified spelling is baked into the cached undecorated + // name, so drop the cache to force re-decoration on next view. + // + _symbolResolver.SimplifyTemplateDefaults = _configuration.SimplifyStlNames; + ClearUndecoratedNameCache(); + } + + if (_configuration.ViewUndecorated != bUndecoratedPrev || + _configuration.SimplifyStlNames != bSimplifyStlPrev) { UpdateFileView(FileViewUpdateAction.FunctionsUndecorateChange); } @@ -1490,6 +1503,32 @@ private void MenuExitItem_Click(object sender, EventArgs e) Close(); } + /// + /// Clears cached undecorated names across all loaded modules so they are + /// recomputed with the current decoration / STL-simplification settings. + /// + private void ClearUndecoratedNameCache() + { + foreach (CModule module in _loadedModulesList) + { + if (module.ModuleData?.Exports is { } exports) + { + foreach (CFunction function in exports) + { + function.UndecoratedName = string.Empty; + } + } + + if (module.ParentImports is { } imports) + { + foreach (CFunction function in imports) + { + function.UndecoratedName = string.Empty; + } + } + } + } + private void CopyFunctionNamesToClipboard(ListView listView, List functions) { var textBuilder = new StringBuilder(); diff --git a/src/WinDepends/Symbols/CStlNameSimplifier.cs b/src/WinDepends/Symbols/CStlNameSimplifier.cs new file mode 100644 index 0000000..65bbf24 --- /dev/null +++ b/src/WinDepends/Symbols/CStlNameSimplifier.cs @@ -0,0 +1,376 @@ +/******************************************************************************* +* +* (C) COPYRIGHT AUTHORS, 2024 - 2026 +* +* TITLE: CSTLNAMESIMPLIFIER.CS +* +* VERSION: 1.00 +* +* DATE: 03 Jun 2026 +* +* Collapses the canonical (default-argument) forms produced by the MSVC +* demangler into their conventional STL spellings, e.g. +* +* std::basic_string,std::allocator > +* -> std::string +* std::map,std::allocator > > +* -> std::map +* std::vector > +* -> std::vector +* +* Only the default-value template forms of the standard library are matched; +* user templates and non-default arguments are left untouched. The transform +* is bracket-aware (it walks balanced angle brackets) because regular +* expressions cannot reliably split nested template argument lists. +* +* THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF +* ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED +* TO THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A +* PARTICULAR PURPOSE. +* +*******************************************************************************/ +using System.Text; + +namespace WinDepends; + +/// +/// Folds MSVC default template-argument forms of the C++ standard library into +/// their conventional typedef spellings. Intended purely for display. +/// +internal static class CStlNameSimplifier +{ + /// + /// Returns with recognized STL default-argument + /// template forms collapsed. On any unexpected input the original string is + /// returned unchanged so the feature can never corrupt a displayed name. + /// + public static string Simplify(string name) + { + if (string.IsNullOrEmpty(name) || name.IndexOf('<') < 0) + { + return name; + } + + try + { + return Transform(name); + } + catch + { + return name; + } + } + + /// + /// Recursively rewrites every template-id found in . + /// Inner argument lists are transformed before the enclosing template is + /// folded, so nested defaults collapse bottom-up. + /// + private static string Transform(string s) + { + var sb = new StringBuilder(s.Length); + int i = 0; + + while (i < s.Length) + { + char c = s[i]; + + if (c == '<') + { + string ident = PeekLastIdentifier(sb); + + // Skip operator<, operator<<, lambdas () and anything + // not preceded by a real identifier - treat '<' literally. + if (ident.Length == 0 || ident.EndsWith("operator", StringComparison.Ordinal)) + { + sb.Append(c); + i++; + continue; + } + + int end = FindMatchingAngle(s, i); + if (end < 0) + { + sb.Append(c); + i++; + continue; + } + + string inner = s.Substring(i + 1, end - i - 1); + List args = SplitTopLevel(Transform(inner)); + + sb.Length -= ident.Length; // remove identifier, re-emit folded + sb.Append(Fold(ident, args)); + i = end + 1; + } + else + { + sb.Append(c); + i++; + } + } + + return sb.ToString(); + } + + /// + /// Applies the STL folding rules for a single template-id whose argument + /// list has already been simplified. Unrecognized templates are rebuilt + /// verbatim. + /// + private static string Fold(string ident, List args) + { + switch (ident) + { + case "std::basic_string": + // , allocator> + if (args.Count == 3 && + IsSpecializationOf(args[1], "std::char_traits") && + IsSpecializationOf(args[2], "std::allocator")) + { + string alias = StringAlias(args[0], isView: false); + if (alias != null) return alias; + } + break; + + case "std::basic_string_view": + // > + if (args.Count == 2 && + IsSpecializationOf(args[1], "std::char_traits")) + { + string alias = StringAlias(args[0], isView: true); + if (alias != null) return alias; + } + break; + + case "std::vector": + case "std::deque": + case "std::list": + case "std::forward_list": + // > + if (args.Count == 2 && IsSpecializationOf(args[1], "std::allocator")) + { + return Rebuild(ident, args[0]); + } + break; + + case "std::set": + case "std::multiset": + // , allocator> + if (args.Count == 3 && + IsSpecializationOf(args[1], "std::less") && + IsSpecializationOf(args[2], "std::allocator")) + { + return Rebuild(ident, args[0]); + } + break; + + case "std::map": + case "std::multimap": + // , allocator>> + if (args.Count == 4 && + IsSpecializationOf(args[2], "std::less") && + IsSpecializationOf(args[3], "std::allocator")) + { + return Rebuild(ident, args[0], args[1]); + } + break; + + case "std::unordered_set": + case "std::unordered_multiset": + // , equal_to, allocator> + if (args.Count == 4 && + IsSpecializationOf(args[1], "std::hash") && + IsSpecializationOf(args[2], "std::equal_to") && + IsSpecializationOf(args[3], "std::allocator")) + { + return Rebuild(ident, args[0]); + } + break; + + case "std::unordered_map": + case "std::unordered_multimap": + // , equal_to, allocator>> + if (args.Count == 5 && + IsSpecializationOf(args[2], "std::hash") && + IsSpecializationOf(args[3], "std::equal_to") && + IsSpecializationOf(args[4], "std::allocator")) + { + return Rebuild(ident, args[0], args[1]); + } + break; + + case "std::unique_ptr": + // > + if (args.Count == 2 && IsSpecializationOf(args[1], "std::default_delete")) + { + return Rebuild(ident, args[0]); + } + break; + } + + return Rebuild(ident, args); + } + + /// + /// Maps a character element type to the std::string / std::string_view family + /// alias, or null when the element type is not a known character type. + /// + private static string StringAlias(string element, bool isView) + { + switch (StripLeadingKeywords(element).Trim()) + { + case "char": return isView ? "std::string_view" : "std::string"; + case "wchar_t": return isView ? "std::wstring_view" : "std::wstring"; + case "char8_t": return isView ? "std::u8string_view" : "std::u8string"; + case "char16_t": return isView ? "std::u16string_view" : "std::u16string"; + case "char32_t": return isView ? "std::u32string_view" : "std::u32string"; + default: return null; + } + } + + /// + /// True when is a specialization of + /// (e.g. "std::allocator<...>"). + /// + private static bool IsSpecializationOf(string arg, string template) + { + arg = StripLeadingKeywords(arg).TrimStart(); + return arg.StartsWith(template, StringComparison.Ordinal) && + arg.Length > template.Length && + arg[template.Length] == '<'; + } + + private static string Rebuild(string ident, params string[] args) + => Rebuild(ident, (IReadOnlyList)args); + + /// + /// Reconstructs "ident<args>" keeping the demangler convention of a + /// space before a closing angle bracket that follows another '>'. + /// + private static string Rebuild(string ident, IReadOnlyList args) + { + if (args.Count == 0) + { + return ident; + } + + var sb = new StringBuilder(ident.Length + 16); + sb.Append(ident).Append('<'); + + for (int i = 0; i < args.Count; i++) + { + if (i > 0) sb.Append(','); + sb.Append(args[i]); + } + + if (args[args.Count - 1].EndsWith(">", StringComparison.Ordinal)) + { + sb.Append(' '); + } + sb.Append('>'); + + return sb.ToString(); + } + + /// + /// Splits a template argument list on top-level commas, ignoring commas + /// nested inside angle brackets, parentheses or square brackets. + /// + private static List SplitTopLevel(string s) + { + var result = new List(); + int depth = 0, start = 0; + + for (int i = 0; i < s.Length; i++) + { + char c = s[i]; + switch (c) + { + case '<': + case '(': + case '[': + depth++; + break; + case '>': + case ')': + case ']': + if (depth > 0) depth--; + break; + case ',': + if (depth == 0) + { + result.Add(s.Substring(start, i - start).Trim()); + start = i + 1; + } + break; + } + } + + result.Add(s.Substring(start).Trim()); + return result; + } + + /// + /// Returns the index of the '>' that matches the '<' at + /// , or -1 when the brackets are unbalanced. + /// + private static int FindMatchingAngle(string s, int open) + { + int depth = 0; + for (int i = open; i < s.Length; i++) + { + if (s[i] == '<') depth++; + else if (s[i] == '>') + { + depth--; + if (depth == 0) return i; + } + } + return -1; + } + + /// + /// Returns the identifier characters immediately preceding the current end + /// of without modifying it. + /// + private static string PeekLastIdentifier(StringBuilder sb) + { + int end = sb.Length; + int i = end - 1; + while (i >= 0 && IsIdentifierChar(sb[i])) + { + i--; + } + return sb.ToString(i + 1, end - (i + 1)); + } + + private static bool IsIdentifierChar(char c) + => char.IsLetterOrDigit(c) || c == '_' || c == ':'; + + /// + /// Strips MSVC type keywords ("class ", "struct ", "enum ", "union ") that + /// may prefix a type when the demangler is not run with NoMsKeyWords. + /// + private static string StripLeadingKeywords(string s) + { + s = s.TrimStart(); + bool changed = true; + while (changed) + { + changed = false; + foreach (string kw in s_typeKeywords) + { + if (s.StartsWith(kw, StringComparison.Ordinal)) + { + s = s.Substring(kw.Length).TrimStart(); + changed = true; + } + } + } + return s; + } + + private static readonly string[] s_typeKeywords = + ["class ", "struct ", "enum ", "union "]; +} diff --git a/src/WinDepends/Symbols/CSymbolResolver.cs b/src/WinDepends/Symbols/CSymbolResolver.cs index 5536a0a..023d8a2 100644 --- a/src/WinDepends/Symbols/CSymbolResolver.cs +++ b/src/WinDepends/Symbols/CSymbolResolver.cs @@ -232,6 +232,12 @@ delegate uint UnDecorateSymbolNameDelegate( public bool SymbolsInitialized { get; private set; } public bool UndecorationReady { get; private set; } + + /// + /// When set, recognized STL default-argument template forms in undecorated + /// names are collapsed to their conventional spellings (e.g. std::string). + /// + public bool SimplifyTemplateDefaults { get; set; } public string DllPath { get; private set; } public string StorePath { get; private set; } @@ -777,7 +783,10 @@ internal string UndecorateFunctionName(string functionName) // Note: DependencyWalker uses UNDNAME.NoAllocateLanguage | UNDNAME.NoMsKeyWords | UNDNAME.NoFunctionReturns | UNDNAME.NoAccessSpecifiers if (UnDecorateSymbolName(functionName, sb, sb.Capacity, UNDNAME.NoMsKeyWords) > 0) { - return sb.ToString(); + string undecorated = sb.ToString(); + return SimplifyTemplateDefaults + ? CStlNameSimplifier.Simplify(undecorated) + : undecorated; } }