C++ Class Form Optimizer
Match C++ class definitions to compiled ROM binaries by testing compiler output variants.
Installation
- Make sure Claude is on your device and in your terminal.
Skills load from
~/.claude/skills/when Claude Code starts up β so you need it on your machine first. If you don't have it yet, install it once with the command below, then runclaudein any terminal to verify.One-time setupnpm i -g @anthropic-ai/claude-codeAlready have it? Skip ahead.
- Paste into Claude Code or into your terminal.
This copies the whole skill folder into
~/.claude/skills/decomp-cpp-class-form-tangosdev/β the SKILL.md plus any scripts, reference docs, or templates the skill ships with. Safe default: works for every skill.Faster alternative (instruction-only skills)
Skips the clone and grabs only the SKILL.md file. Don't use this if the skill ships Python scripts, reference markdowns, or asset templates β they won't be downloaded and the skill will fail when it tries to load them.
Quick install (SKILL.md only)Sign up to copy - Restart Claude Code.
Quit and reopen Claude Code (or any other agent that loads from
~/.claude/skills/). New skills are picked up on startup. - Just ask Claude.
Skills auto-activate when your request matches the skill's description β no slash command needed. Trigger phrases live in the skill's own frontmatter; you can read them in the βWhat this skill doesβ section above.
Prefer to read the source first? Open on GitHub.
When Claude uses it
Choose the C++ source form that makes mwccarm 2004/b56 emit what the SM64DS ROM actually contains - destructor variant order (D1/D0/D2), vtable and RTTI anchoring, constructor inlining, struct copies, bool widening, and virtual-vs-direct calls. Use when converting a shadow struct into a real C++ class, when D0/D1 are missing or emitted in the wrong order, when _ZTV/_ZTI is absent or multiply-defined, when a .c -> .cpp rename costs bytes, or when a class compiles but the whole-range link will not reproduce.
What this skill does
Making mwccarm emit the ROM's shape
Byte-matching a real C++ class is a question about source form, not about being "more correct". The compiler is an oracle β ask it rather than reasoning about the ABI.
Ask the compiler, don't hand-mangle
python tools/mangle.py candidate.cpp
python tools/mangle.py candidate.cpp --expect _ZN5Actor8BehaviorEv
python tools/mangle.py candidate.cpp --mangled-only --json
It compiles with the pinned 2004/b56 C++ flags and reads defined GLOBAL/WEAK ELF
symbols, so substitutions, thunks, ctor/dtor variants and static data are the
compiler's answer, not a guess. It uses match.DEFAULT_FLAGS, which differs from
rombuild.CFLAGS (-w illpragmas vs -Cpp_exceptions off) β verified not to change
the emitted symbol set, so it is sound for the checks below.
Ordering cannot be read from st_value. mwccarm emits one .text section per
function, so every symbol's st_value is 0. Order lives in the section index.
Destructor variants: the form decides the order
The ROM typically has D1 then D0 and no D2 (e.g. ov045: D1 0x021111a0 size 0x44,
D0 0x021111e4 size 0x58, landing exactly on the next function). Measured:
| source form | instantiated in TU? | emits | order |
|---|---|---|---|
X::~X() {} out of line | no | D2, D0, D1 + _ZTV | D0 before D1 β wrong |
X::~X() {} out of line | yes (new X()) | D2, D0, D1 + _ZTV | D0 before D1 β wrong |
virtual ~X() {} in class body | no | nothing at all | no variants, no _ZTV |
virtual ~X() {} in class body | yes (new X()) | D1, D0 + _ZTV | D1 then D0 β matches |
Two conditions, both necessary: define the destructor inline in the class body, and
make the TU actually instantiate the class. The inline form also emits no D2, which
removes a homeless _ZNβ¦D2Ev that has no symbols.txt entry and cannot be dropped
under -nodead.
In the winning form the destructor pair is emitted first, ahead of everything else β which is what a TU whose ROM range starts with D1 needs.
Vtable and RTTI anchoring
mwcc anchors _ZTV/_ZTI/_ZTS to the TU that defines the destructor out of line.
Consequences:
- Move the destructor inline and the anchor disappears; nothing emits the vtable unless something odr-uses it (instantiation).
- A TU that declares
extern int _ZTV15Foo[];imports the vtable and will never define it β and will not emit the destructor variants the vtable would reference either. If the ROM puts the vtable in this TU, thatexternis the bug. - Conversely, adding a virtual declaration can make a file become the key-function TU
and start emitting
_ZTV, which drops it from the build whilebuild_pinstill saysTrue. Checkeligible-names.txtafter adding any virtual. It can also un-build a siblingD0file you never touched βdelete p, notp->~X(). - A
_ZTV*relocation's addend must lose 8 on rebinding: mwcc's vtable symbol addresses the object's start,symbols.txt's addresses the slot array. In source, the same fact appears as+ 2on anint *. Getting it wrong links clean and corrupts 34 modules.objisolate's addend check is the only gate that sees it.
Constructors
An implicit constructor gets inlined, so a TU that expects a real bl to
_ZN8PlatformC2Ev will not produce one. Declaring X(); and never defining it is the
fix β read this before migrating any constructor.
A hand-rolled spawn like
p = operator new(0x328); PlatformC2Ev(p); p[0] = _ZTV + 2;
is the un-migrated form. Converting it to return new X(); is what odr-uses the
vtable and unlocks the destructor pair above.
C-vs-C++ codegen differences that cost bytes
- Struct copy is scalarized in C++ but a block move in C β typically 12 bytes short.
Fix: copy through a struct whose only member is an array
(
struct M { int w[12]; };), which restores the ROM'sldm/stmpair. Applying this mechanically to a corpus of C++ divergences recovered 66% of word-level mismatches with zero residual. boolwidening:(int)((long long)(a == b))isintin C,boolin C++ β a.c->.cpprename cost 17 words. Alsoint f = (x==k); if (f)is 3 instructions longer thanif (x==k); the temporary is load-bearing.- A qualified call suppresses virtual dispatch:
obj.Base::Method()emits a directbl. This goes both ways β read the shadow struct before assuming. - Compound assignment costs an instruction:
x += kCSEs the field address. Raw-cast-vs-member is decided per function; only the byte gate settles it. - A bare
externon an already-mangled name inside a//cppfile mangles it twice. Wrap ROM symbols spelled by their mangled name inextern "C". extern "C" { int x; }DEFINESx, it does not declare it, and collides with the gap object. Useextern.
Language mode is per file, and it is not the extension
The first-line //cpp marker swaps -lang c99 for -lang c++. build_pin.py calls it
"the whole test"; the extension is never consulted. Whole-corpus measurement: 90.6% of
C99-matching functions reproduce byte-identically under -lang c++ unchanged (94.0%
excluding __sinit_ files), so pure C++ is reachable. The failures are source hygiene,
not codegen: empty-paren prototypes (func_x(); means "unspecified args" in C and "no
args" in C++), this used as a parameter name, enum { false, true };, void* needing
an explicit cast.
Never trust bytes alone
match.compare wildcards every relocated word, so a class method can reproduce the
bytes while calling the wrong function. Require all three: byte comparison,
objisolate (relocation type/addend), and reloc_audit (destination identity). A
result that could not run the audits is "not verified", never "probably fine".
Related skills
Skill Builder & Optimizer
anthropics
Create, edit, and optimize Claude skills with performance testing and benchmarking.
CFO Financial Advisor
alirezarezvani
Build financial models, analyze unit economics, and plan fundraising strategy for startups.
Claude Code Session History Finder
daymade
Search and recover code from previous Claude Code sessions and conversations.
Network & Streaming Debugger
daymade
Debug network and streaming bugs using evidence-driven investigation and layered isolation experiments.