Files
ppsspp/docs/CodeWarriorMangling.md
T
Henrik RydgårdandClaude Opus 5 dfc04f3578 Demangle: handle CodeWarrior templates, function pointers and @-symbols
Checked against two PSP binaries that shipped with intact symbol tables,
which turned up several constructs the format's usual description doesn't
mention:

- Template arguments are written literally inside the length-prefixed name
  ("39CList<Q38hlScreen5Brwsr13CContentsUnit>"), not with a "__PT" prefix,
  and they nest. Function templates put theirs in the base name instead,
  followed by the return type.
- A family of "@"-decorated symbols for things with no C++ name: thunks
  ("@12@__dt__3SonFv"), string literals, function-local statics and their
  guard variables. Plus __vt__/__RTTI__/__sinit_, printed in the same style
  as the Itanium special names.
- Types are now built as a split declarator, so a pointer to a function
  comes out as "int (*)(int)" rather than "int (int) *".

Also stop the lenient pass from turning plain C names with a "__" in them
into nonsense - "I3dClut__FlushCache" became "I3dClut(long, ...)". It now
requires a class qualifier, which costs nothing: over ~10000 symbols the
lenient pass rescued none and only produced those false positives.

Symbol map names go from 128 to 256 characters, since a demangled name
keeps its parameters and templates make short work of 128.

docs/CodeWarriorMangling.md describes the format, marking the parts that
are inferred from cfront rather than attested in a real binary.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SF5eS5QDNexLksRDeDZvwY
2026-08-29 23:23:57 +02:00

8.9 KiB

Metrowerks CodeWarrior C++ symbol mangling (PSP)

Some PSP titles were built with Metrowerks CodeWarrior rather than the SDK's GCC, so their symbols aren't Itanium-mangled and _Z-based demanglers do nothing with them. PPSSPP demangles them in Common/Data/Text/Demangle.cpp (DemangleCodeWarrior), which is what makes the symbol map readable for those games.

The scheme is a descendant of the AT&T cfront mangling, and the basics match the Macintosh C++ ABI. It deviates in enough places that following that document alone produces wrong answers, so this is a description of what PSP binaries actually contain.

How this was worked out

From two PSP executables that shipped with intact symbol tables - one small C++ test program, one large application of about 10,000 symbols - by demangling every symbol and checking the result against the disassembly and against the bytes each data symbol points at.

Constructs the corpus did not contain are marked (inferred) below. They come from the cfront scheme, are implemented, and are believed right, but nothing here proves them.

Overall shape

A mangled symbol is:

<basename> __ [<class>] [<cv>] F <parameters> [_ <return type>]

with the class, the cv-qualifiers, the F and everything after it all optional. A few examples, building up:

Mangled Demangled
run_tests__Fv run_tests()
getDistance__6KzUtilFP7st_unitP7st_unit KzUtil::getDistance(st_unit *, st_unit *)
what__Q23std9exceptionCFv std::exception::what() const
count__Q23foo3bar foo::bar::count

The last one has no F, so it isn't a function at all - it's a static data member, and there is no parameter list to print. A name with neither a class nor an F is not mangled.

Finding the separator

The __ separator is genuinely ambiguous. A base name can itself start with underscores (__SetupFrameInfo__FP12ThrowContext...), contain a __ of its own (__TableUnit__SetValue<i,1>__5shTbbF...), and plain C code in the same binary is full of names like I3dCacheManager__GarbageCollect that are not mangled at all.

There is no way to resolve this from the grammar; PPSSPP tries every __ in the symbol and keeps the first split whose right-hand side parses completely. Only if none does will it accept a split whose parameters didn't decode - and then only when a class qualifier is present, because otherwise every C name with a double underscore in it decodes as a garbage function signature.

Names

An identifier is written as its length in decimal followed by its characters: 6KzUtil.

A qualified name is Q<count> followed by that many identifiers: Q23std9exception is std::exception (2 components, 3std and 9exception). The count is a single digit; ten or more components are written Q_<count>_ (inferred).

Template arguments

Template arguments are written literally between < and >, inside the length-prefixed name - the length covers the whole thing, brackets included. (The Macintosh ABI document describes a __PT prefix instead. PSP binaries do not use it.)

39CList<Q38hlScreen5Brwsr13CContentsUnit>     shList::CList<hlScreen::Brwsr::CContentsUnit>

Here 39 is the length of CList<Q38hlScreen5Brwsr13CContentsUnit>. Each argument is either a mangled type, or a plain integer for a non-type parameter, separated by commas:

Mangled Demangled
15CSimpleChar<24> CSimpleChar<24>
61ForwardIterator<16TABLE_VAR_MEMBER,21TABLE_VAR_SECT_HEADER,v> ForwardIterator<TABLE_VAR_MEMBER, TABLE_VAR_SECT_HEADER, void>
47CList<Q26ssTool29tag_<Q26shFont12SysCmdString>> CList<ssTool::tag_<shFont::SysCmdString>>

They nest, so a parser must match brackets rather than scanning for the next >, and split arguments only on top-level commas.

A function template puts its arguments in the base name instead, and - unlike a normal function - encodes its return type after a trailing _:

sort<Pf>__3stdFPfPf_v          void std::sort<float *>(float *, float *)

Special base names

Base name Meaning
__ct Constructor; the name is taken from the class
__dt Destructor
__op<type> Conversion operator, e.g. __opCi is operator const int()
__<code> Overloaded operator, see below

Operator codes are the cfront set: nw dl nwa dla for new/delete, pl mi ml dv md for arithmetic, apl ami amu adv amd aad aor aer als ars for the compound assignments, eq ne lt gt le ge for comparisons, aa oo nt for logic, ad or er co ls rs for bitwise, and as pp mm cl vc rf rm cm for = ++ -- () [] -> ->* ,.

So __as__9ANIMEDataFRC9ANIMEData is ANIMEData::operator=(const ANIMEData &).

Types

Type codes are read left to right, each one modifying what follows.

Code Type
v void
b bool
c char
s short
i int
l long
x long long
f float
d double
r long double (inferred)
w wchar_t (inferred) - a Metrowerks addition to the cfront set
e ... varargs, always last
P pointer to
R reference to
C const
V volatile
U unsigned
S signed (inferred)
A<n>_ array of n (inferred)
F<params>_<ret> function
<len><name> class
Q<n>... qualified class

PCc is const char *; CPc is char * const. A pointer to a function or an array needs the declarator wrapped rather than a * appended - PFi_i is int (*)(int), and PPFv_v is void (**)().

Two forms back-reference an earlier parameter of the same function (inferred):

Form Meaning
T<index> Same type as parameter <index>, 1-based
N<count><index> <count> more parameters, each the type of parameter <index>

So foo__FPCcUiN21 is foo(const char *, unsigned int, const char *, const char *).

cv-qualifiers on the function

A C or V sits between the class and the F, and qualifies the member function rather than a parameter: InitRun__Q28shCamera11TStillParamVFv is shCamera::TStillParam::InitRun() volatile.

Compiler-generated symbols

CodeWarrior emits a family of symbols for things with no C++ name of their own. These wrap a mangled name rather than being one, and none of them are described in the ABI document.

Form Meaning Example
__vt__<class> Vtable __vt__Q23std9exception
__RTTI__<class> Typeinfo record __RTTI__Q23std9exception
__sinit_<file> Static initializers for a translation unit __sinit_hl_app.cpp
__sterm_<file> Static destructors
@<n>@<symbol> this-adjusting thunk, n bytes @12@__dt__3SonFv
@STRING@<symbol>[@<n>] A string literal used inside that function @STRING@Get_BGM__Q25hlBHC4SBhcFi@0
@LOCAL@<symbol>@<var>[@<n>] A function-local static @LOCAL@sort<Pf>__3stdFPfPf_v@shuffle@0
@GUARD@<var>$<n> Its "already constructed" flag @GUARD@app$16079
@<n> An anonymous string constant @10046

The trailing @<n> on @STRING@ and @LOCAL@ is a discriminator, present only when a function has more than one of them - so a parser has to treat it as optional, and must not mistake the @ before a @LOCAL@ variable name for it.

@<n>@ thunks really are thunks: the one above is 8 bytes of code that does addiu a0, a0, -12 and jumps, i.e. adjusts this for a secondary base. @STRING@ symbols point at ordinary string data - usually the __FILE__ an assertion expanded to.

Other decorations

Two more show up inside otherwise ordinary names. Neither needs decoding, but both are worth recognising:

  • <name>$<digits><file> - a class or variable with internal linkage, tagged with where it was declared: ClutScreen$11229hl_cplayer_effect_renderer_cpp. These make for very long symbols, since the tag repeats everywhere the type appears.
  • <len>@unnamed@<file>@ - used as a qualifier for file-scope statics, i.e. an unnamed namespace. ARWMENU_MODEL_NAME__34@unnamed@hl_editor_arrow_menu_cpp@ is a data symbol in one.

Hazards

  • A truncated name is unrecoverable. Some symbol tables cap names (127 characters is a common limit), and templates blow past that easily. Once a name is cut, the length prefixes no longer match what's left and nothing after the cut can be trusted - better to reject the symbol than to print a plausible-looking guess.
  • Not everything with a __ is mangled. C code linked into the same binary uses __ as a word separator freely. Requiring a class qualifier before accepting a partial parse is what keeps I3dClut__FlushCache from turning into I3dClut(long, ...).
  • e is a real parameter, not a parse failure. Printf__Q26shFont4FontFiiPCce ends in varargs; (int, int, const char *, ...) is the correct answer, not a partial one.