Metadata-Version: 2.4
Name: multi-demangle
Version: 2.0.0
License-File: LICENSE
Summary: A library to demangle symbols from various languages and compilers.
Keywords: demangler,binary
Home-Page: https://github.com/AppThreat/multi-demangle
License-Expression: MIT
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM

# multi-demangle

[![CI](https://github.com/AppThreat/multi-demangle/actions/workflows/CI.yml/badge.svg)](https://github.com/AppThreat/multi-demangle/actions/workflows/CI.yml)

Demangling support for various languages and compilers, usable as a Rust crate or a
Python extension module. Fork of [symbolic-demangle](https://github.com/getsentry/symbolic/tree/10.2.1/symbolic-demangle).

Currently supported languages are:

| Language      | Mangling schemes / notes                                                                                           | Cargo feature                         |
| ------------- | ------------------------------------------------------------------------------------------------------------------ | ------------------------------------- |
| C++           | Itanium ABI (GCC, Clang), GNU v2, CodeWarrior, and MSVC                                                            | `cpp`, `gnuv2`, `codewarrior`, `msvc` |
| Rust          | Both `legacy` and `v0` schemes                                                                                     | `rust`                                |
| Scala Native  | Via the unknown-language fallback (symbols prefixed `_SM`)                                                         | `scala-native`                        |
| Swift         | Up to Swift 6.3.3, using a vendored Swift demangler                                                                | `swift`                               |
| D             | D ABI mangling (`_D…`), incl. function types and templates                                                         | `dlang`                               |
| Fortran       | gfortran `mod_MOD_proc` and Intel `mod_mp_proc_`; the plain g77 `name_` form only via explicit request (see below) | `fortran`                             |
| Kotlin/Native | `_kfun:` symbols with parameter types                                                                              | `kotlin-native`                       |
| Ada (GNAT)    | `pkg__sub` encoding with escapes, operators, and markers                                                           | `ada`                                 |
| ObjC          | Selectors plus runtime metadata symbols (`_OBJC_CLASS_$_…`, `_OBJC_IVAR_$_…`, selector references)                 | always on                             |

All of the above features are enabled by default. Disabling them trims the
corresponding demangler (and, for `swift`, the vendored C++ sources) from the build.

As the demangling schemes for the languages are different, the supported demangling features are
inconsistent. For example, argument types were not encoded in legacy Rust mangling and thus not
available in demangled names.

## Rust usage

The crate exposes a `Demangle` trait on `symbolic_common::Name`, along with
`DemangleOptions` to control how verbose the output is:

```rust
use symbolic_common::{Language, Name};
use multi_demangle::{Demangle, DemangleOptions};

let name = Name::from("__ZN3std2io4Read11read_to_end17hb85a0f6802e14499E");

// Detect the language of a mangled symbol.
assert_eq!(name.detect_language(), Language::Rust);

// Demangle with a full, verbose signature.
assert_eq!(
    name.try_demangle(DemangleOptions::complete()),
    "std::io::Read::read_to_end"
);

// The shortcut free function demangles with complete options and
// falls back to the input if demangling fails.
assert_eq!(multi_demangle::demangle("_ZN3foo3barEv"), "foo::bar()");
```

The `cli` cargo feature (on by default) pulls in the argument parser for the
binary. Library-only consumers can drop it by re-enabling the backends
explicitly:

```toml
multi-demangle = { version = "...", default-features = false, features = [
  "cpp", "gnuv2", "codewarrior", "msvc", "rust", "scala-native", "swift",
] }
```

### Batch demangling

Symbol tables repeat the same symbol many times (dynsym, symtab, version
tables, and GOT/PLT maps), so the batch API demangles each distinct symbol at
most once and preserves input order:

```rust
use multi_demangle::{demangle_iter, DemangleOptions};

let symbols = ["_ZN3foo3barEv", "libc.so.6", "_ZN3foo3barEv"];
let demangled = demangle_iter(symbols, DemangleOptions::complete());
assert_eq!(&demangled[0], "foo::bar()");
assert_eq!(&demangled[1], "libc.so.6");
assert_eq!(&demangled[2], "foo::bar()");
```

`demangle_iter` computes the whole batch eagerly and returns a `Vec` (in
input order). `demangle_one` is the single-symbol pipeline the batch is built
on (it is what `multi_demangle::demangle` delegates to), exposed so consumers
can share it with their own batching. Enabling the `parallel` cargo feature
(off by default) demangles the distinct symbols on the rayon thread pool.

```toml
multi-demangle = { version = "...", features = ["parallel"] }
```

### Structured demangling

`demangle_structured` extracts typed fields from the demangled rendering —
namespace path, leaf name, entity kind, generics, parameters, return type,
and compiler disambiguation hashes — so consumers do not have to re-parse
text:

```rust
use symbolic_common::Name;
use multi_demangle::{Demangle, DemangleOptions};

let info = Name::from("_ZN3std2io4Read11read_to_end17hb85a0f6802e14499E")
    .demangle_structured(DemangleOptions::complete())
    .unwrap();
assert_eq!(info.namespace, ["std", "io", "Read"]);
assert_eq!(info.name, "read_to_end");
assert_eq!(info.kind, multi_demangle::DemangledKind::Method);
assert_eq!(info.hash.as_deref(), Some("hb85a0f6802e14499"));
assert_eq!(info.parameters, None); // legacy Rust encodes no parameter types
```

`DemangledKind` classifies the entity (best-effort, modeled on the primary
consumer's tables): `Function`, `Method`, `Closure`, `Glue` (CRT/linker glue
and drop glue), `Intrinsic`, `MethodThunk`, `VirtualTable`, `TypeInfo`,
`ObjCMethod { class_method }`, `StaticVariable`, or `Other`.

`<Type as Trait>::` and `<impl Trait for Type>::` prefixes are reduced to the
implementing type in the namespace, and trailing Rust hashes plus `.llvm.N`
clone counters are captured into `hash` instead of being silently stripped.

Where a backend exposes its parse tree, the fields come from the AST rather
than text: MSVC symbols are walked through `msvc_demangler`'s parse tree
(which is how a data symbol like `?value@ns@@3HA` is correctly a
`StaticVariable` and `??_7Bar@@6B@` a `VirtualTable`), Swift symbols through
the vendored demangler's node tree (accessors, initializers, and closures
keep their kinds; accessor names resolve to the wrapped property), and
Itanium kinds come from the mangling grammar's own prefixes (`_ZGV` guard
variables, `_ZTV`/`_ZTC` vtables, `_ZTh`/`_ZTv` thunks). Text-derived
extraction remains the fallback for every language.

One stated Itanium limitation: thunk symbols (`_ZTh`/`_ZTv`) classify with
the right kind, but their target identity is not extracted — the leaf name
stays the full brace rendering (`{virtual override thunk(...)}`) since the
demangled form is descriptive and no Itanium AST is available to walk.

### New-language backends (D, Fortran, Kotlin/Native, Ada)

The D backend is a port of LLVM's D demangler extended with the full type
grammar from the [D ABI specification](https://dlang.org/spec/abi.html) —
function types, member functions, compound types, type modifiers, template
instances, and back references. It is the only D demangler implemented in
Rust. Function symbols render as `module.func(params)` without the return
type (matching the reference demanglers), variables as `type module.var`, and
template instances as `name!(args)`:

```rust
use symbolic_common::{Language, Name};
use multi_demangle::{Demangle, DemangleOptions};

assert_eq!(Name::from("_Dmain").detect_language(), Language::D);
assert_eq!(
    Name::from("_D6module4funcFZv").try_demangle(DemangleOptions::complete()),
    "module.func()"
);
assert_eq!(
    Name::from("_D6module4Test6methodMFiZi").try_demangle(DemangleOptions::complete()),
    "module.Test.method(int)"
);
```

Fortran module symbols demangle to `module::proc` for both gfortran
(`__mod_MOD_proc`, with or without platform underscores) and Intel
(`mod_mp_proc_`) conventions. The plain g77 form (`init_`, `my_sub__`) is
_explicit-only_: any C symbol can end in `_`, so auto-detection never claims
it. Use `demangle_as("fortran", …)` (Rust) or `--language fortran` (CLI) to
demangle that form.

```rust
use multi_demangle::{demangle_as, Demangle, DemangleOptions};

// Auto-detection leaves `init_` alone (it could be a C symbol)...
assert_eq!(multi_demangle::demangle("init_"), "init_");
// ...but the explicit entry point demangles it.
assert_eq!(
    demangle_as("fortran", "init_", DemangleOptions::complete()),
    Some("init".to_string())
);
```

Kotlin/Native symbols still carry the readable `kfun:` prefix (verified
against the 2.0.20x compilers; see `contrib/fixtures/kotlin/`), in the
modern spelling `kfun:<pkg>#<member>(<params>){<bounds>}<ret>`. They render
with their parameter and return types and `kotlin.` prefix elision:
`kfun:com.example.Counter#increment(kotlin.Int){}kotlin.Int` becomes
`com.example.Counter.increment(Int): Int`. Compiler markers (`#static`,
`#internal`) render as trailing name segments, and `-trampoline` thunks keep
a ` [trampoline]` marker so they do not alias the function they dispatch to.

Ada (GNAT) symbols decode the `pkg__sub` encoding — `_ada_` prefixes, `__N`
body suffixes, elaboration procedures (`corpus___elabb` →
`corpus'Elab_Body`), compiler-generated task companions (`TB` task body,
`IP` initialization procedure, `E`/`Z` variables), `U`/`W` character
escapes, anonymous blocks, and operator names (`module__Oadd` →
`module."+"`).

ObjC support now also recognizes runtime metadata symbols —
`_OBJC_CLASS_$_Foo`, `_OBJC_METACLASS_$_Foo`, `_OBJC_IVAR_$_Foo.bar`, and
emitted selector references (`l_OBJC_SELECTOR_REFERENCES_…`) — passing the
symbols through unchanged but classifying them with typed kinds
(`objc_class`, `objc_metaclass`, `objc_ivar`, glue) in the structured API.
These dominate non-Swift Mach-O symbol tables.

### Symbol hygiene

On top of demangling, the crate provides cheap, prefix-based helpers for the
questions consumers face around demangling: is this symbol mangled, in which
language, and which linker decorations wrap it?

```rust
use multi_demangle::{
    classify_symbol, detect_language, looks_mangled, normalize_symbol, Decoration,
    SymbolStatus,
};

// Cheap mangling check; never attempts a demangling pass.
assert!(looks_mangled("_$s8mangling12GenericUnionO3FooyACyxGSicAEmlF"));
assert!(!looks_mangled("libc.so.6"));

// Language detection (includes Scala Native, which has no Language variant).
assert_eq!(detect_language("_ZN3foo3barEv"), Some("cpp"));
assert_eq!(detect_language("libc.so.6"), None);

// Classification without demangling.
let status = classify_symbol("__imp_?foo@bar@@YAXXZ");
assert_eq!(
    status,
    SymbolStatus::Decorated {
        decoration: Decoration::ImportPointer,
        inner: Box::new(SymbolStatus::Mangled(symbolic_common::Language::Cpp)),
    }
);

// Display-oriented normalization: legacy Rust `$`-escapes, Rust hash
// suffixes, import pointer rewriting, and pseudo-symbol mapping.
assert_eq!(
    normalize_symbol("std::io::Read::read_to_end::hb85a0f6802e14499"),
    "std::io::Read::read_to_end"
);
assert_eq!(
    normalize_symbol("__imp__Z1fv"),
    "__declspec(dllimport) _Z1fv"
);
```

Two `Normalizer` pass sets are available: `Normalizer::display()` (the default
of `normalize_symbol`) cleans names for humans, while `Normalizer::matching()`
additionally strips `.llvm.` clone suffixes, PLT/GOT call stubs, and ELF
version suffixes — and strips import pointers instead of rewriting them — so
results match the other binary's export table:

```rust
use multi_demangle::Normalizer;

assert_eq!(
    Normalizer::matching().normalize("memcpy@plt"),
    "memcpy"
);
assert_eq!(
    Normalizer::matching().normalize("__imp_CreateFileW"),
    "CreateFileW"
);
```

[`Demangle::try_demangle_normalized`](crate::Demangle::try_demangle_normalized)
combines demangling with a normalizer fallback: a symbol that cannot be
demangled goes through the given passes instead of being returned unchanged
(successful demangled output is never normalized).

```rust
use symbolic_common::Name;
use multi_demangle::{Demangle, DemangleOptions, Normalizer};

assert_eq!(
    Name::from("__imp__ZN3foo3barEv")
        .try_demangle_normalized(DemangleOptions::complete(), &Normalizer::display()),
    "__declspec(dllimport) _ZN3foo3barEv"
);
```

## Python usage

Install the pypi package `multi-demangle`:

```
pip install multi-demangle
```

The module exposes `demangle_symbol` together with a `DemangleOptions` class:

```
>>> import multi_demangle
>>> print(multi_demangle.demangle_symbol("_ZN3foo3barEv"))
foo::bar()

>>> # name-only output, without parameters or return types
>>> opts = multi_demangle.DemangleOptions.name_only()
>>> print(multi_demangle.demangle_symbol("_ZN3foo3barEv", options=opts))
foo::bar

>>> # pick individual options via keyword arguments
>>> opts = multi_demangle.DemangleOptions(return_type=False, parameters=True)
>>> print(multi_demangle.demangle_symbol("__pl__FRC9CRelAngleRC9CRelAngle", options=opts))
operator+(CRelAngle const &, CRelAngle const &)
```

`demangle_symbol` returns the original string unchanged when the language cannot
be detected or demangling fails.

### Batch demangling

`demangle_symbols` demangles a whole batch in one call, releasing the GIL for
the duration. It accepts any iterable of strings — lists, tuples, generators,
`map` objects — so hot loops can feed it without materializing first.
Duplicate symbols are demangled once by default and share a single string
object across their occurrences; results keep the input order and unmangled
symbols pass through unchanged:

```
>>> multi_demangle.demangle_symbols(["_ZN3foo3barEv", "libc.so.6", "_ZN3foo3barEv"])
['foo::bar()', 'libc.so.6', 'foo::bar()']

>>> # pass unique=False to demangle every position independently
>>> multi_demangle.demangle_symbols(["_Z1hic", "libc.so.6"], unique=False)
['h(int, char)', 'libc.so.6']

>>> # options behave like demangle_symbol
>>> multi_demangle.demangle_symbols(["_ZN3foo3barEv"], options=multi_demangle.DemangleOptions.name_only())
['foo::bar']
```

This replaces the per-symbol calls in hot loops (full symbol tables, PE import
tables, Mach-O binding/stub maps) with a handful of batch calls. Type stubs
ship with the wheel (`multi_demangle.pyi`), so type checkers see the full API
including the keyword-only `unique` parameter.

### Language detection and symbol hygiene

```
>>> multi_demangle.detect_language("_ZN3foo3barEv")
'cpp'
>>> multi_demangle.detect_language("libc.so.6") is None
True

>>> # cheap prefix-based check; never attempts a demangling pass
>>> multi_demangle.looks_mangled("_$s8mangling12GenericUnionO3FooyACyxGSicAEmlF")
True

>>> multi_demangle.normalize_symbol("std::io::Read::read_to_end::hb85a0f6802e14499")
'std::io::Read::read_to_end'

>>> multi_demangle.classify_symbol("_Z1hic@GLIBC_2.2.5")
{'status': 'mangled', 'language': 'cpp', 'decorations': [{'kind': 'version', 'value': 'GLIBC_2.2.5'}]}

>>> info = multi_demangle.demangle_symbol_ex("__imp_?h@@YAXH@Z")
>>> info["status"], info["language"], info["decorations"]
('mangled', 'cpp', [{'kind': 'import-pointer'}])
```

`demangle_symbol_ex` returns a dict with `mangled`, `demangled`, `status`
(`"mangled"`, `"unmangled"`, or `"unsupported"`), `language`, and an
outermost-first `decorations` list; `classify_symbol` returns the same
classification without demangling. Passing
`multi_demangle.DemangleOptions(normalize=True)` applies the display hygiene
passes to the fallback when demangling does not succeed, and
`Normalizer.matching()` provides the pass set for cross-symbol matching
(`memcpy@plt` → `memcpy`, `__imp_CreateFileW` → `CreateFileW`).

### Structured demangling

```
>>> info = multi_demangle.demangle_symbol_structured("_ZN3std2io4Read11read_to_end17hb85a0f6802e14499E")
>>> info.language
'rust'
>>> info.name
'read_to_end'
>>> info.namespace
['std', 'io', 'Read']
>>> info.kind
'method'
>>> info.hash
'hb85a0f6802e14499'
>>> info.parameters is None  # legacy Rust encodes no parameter types
True
```

`demangle_symbol_structured` returns a `DemangledInfo` with read-only
getters (`display`, `simple`, `namespace`, `name`, `kind`, `parameters`,
`return_type`, `hash`, `template_args`, `is_generic`, `mangled`, and
`class_method` for ObjC) plus `to_dict()` for JSON serialization. It returns
`None` for symbols that are not mangled in any known scheme.

## CLI

A `c++filt`-style command line tool ships with the crate. Install it with a
Rust toolchain (or use `cargo run --` from a checkout):

```
cargo install multi-demangle
```

With arguments, each argument is demangled to one output line. Without
arguments, the tool runs in **filter mode**: lines are read from stdin, every
whitespace-separated token that looks mangled is demangled, and everything
else passes through unchanged — so it composes with `nm` / `objdump`
pipelines:

```
$ multi-demangle _ZN3foo3barEv
foo::bar()

$ nm libfoo.so | multi-demangle
$ nm libfoo.so | sort | uniq -c | multi-demangle -n --normalize
```

Hyphen-prefixed symbols such as ObjC selectors are accepted as values, so the
obvious invocation just works (and `--` works too):

```
$ multi-demangle '-[Foo bar:blub:]'
-[Foo bar:blub:]
```

Options:

| Flag                                   | Effect                                                                                                                                                                                                                               |
| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `-n, --name-only`                      | names only, no parameters or return types                                                                                                                                                                                            |
| `--no-parameters` / `--no-return-type` | individual output toggles                                                                                                                                                                                                            |
| `-l, --language <LANG>`                | force a backend instead of auto-detecting (`cpp`, `rust`, `swift`, `objc`, `objcpp`, `d`, `fortran`, `kotlin-native`, `ada`, `scala-native`); forcing `fortran` also enables the plain g77 `name_` form                              |
| `--normalize`                          | apply the symbol hygiene passes (`__imp_`, `@plt`, ELF versions, Rust hash suffixes and `$`-escapes, `.llvm.` clone suffixes, pseudo-symbols) to symbols that cannot be demangled, then demangle the cleaned symbol when it succeeds |
| `-s, --structured`                     | print one JSON record per symbol with its status, language, linker decorations, and the structured fields (name, namespace, kind, parameters, return type, generics, hash)                                                           |
| `--list-languages`                     | print the supported languages and the backends enabled in this build                                                                                                                                                                 |
| `--color=auto/always/never`            | colorize successfully demangled output (auto is the default)                                                                                                                                                                         |

`multi-demangle --version` prints the crate version together with the enabled
backends. Exit code is `0` on success — including when nothing looked mangled
— and `1` on I/O errors.

```
$ multi-demangle -s "_Z1hic@GLIBC_2.2.5"
{"mangled":"_Z1hic@GLIBC_2.2.5","demangled":"_Z1hic@GLIBC_2.2.5","status":"mangled","language":"cpp","decorations":[{"kind":"version","value":"GLIBC_2.2.5"}]}
```

In filter mode with `--structured`, records are emitted only for tokens that
look like symbols or that the pipeline changed — under `--normalize`, a
cleaned token such as `bar.llvm.12345` is reported — while plain addresses,
type letters, and words are skipped.

`--normalize` never touches directly successful demangled output; the passes
run on the symbols the demanglers rejected, and the cleaned symbol is then
demangled once more — a version-suffixed `_Z1hic@GLIBC_2.2.5` comes out as
`h(int, char)`. Because `.llvm.` clone suffixes and legacy Rust `$`-escapes
appear on names that do not classify as mangled, filter mode processes every
token while `--normalize` is active:

```
$ multi-demangle --normalize bar.llvm.12345
bar
$ multi-demangle "_Z1hic@GLIBC_2.2.5"
_Z1hic@GLIBC_2.2.5
$ multi-demangle --normalize "_Z1hic@GLIBC_2.2.5"
h(int, char)
```

## Development

Use `uv` package manager.

```
uv tool install maturin
maturin develop --all-features
```

Run the Rust test suite (includes the vendored Swift demangler build):

```
cargo test --all-features
```

Run the Python tests against the locally built module:

```
maturin develop --all-features
pytest python/tests
```

Run the criterion benchmarks for the batch pipeline (uses the real-symbol
dumps in `tests/corpus/`; regenerate them with
`scripts/collect-corpus.sh` when the producing toolchains change):

```
cargo bench
```

The Swift demangler is a minimal subset of the Swift standard library sources
vendored under `vendor/swift`; see [vendor/swift/README.md](vendor/swift/README.md)
for how it is maintained.

### Updating the vendored Swift demangler

A single command syncs `vendor/swift` from upstream (shallow, blobless, sparse
clones), auto-adds headers new to the demangler's dependency graph, records
provenance in `vendor/swift/SYNC.md`, and runs the validation gauntlet
(`cargo test --all-features`, the Python tests, and an ASan/UBSan pass over
the real-symbol corpus):

```
scripts/sync-swift.sh                # newest swift-*-RELEASE tag
scripts/sync-swift.sh swift-6.4.0    # or an explicit tag
```

A monthly CI workflow (`swift-sync-reminder`) opens an issue listing upstream
commits that touch `lib/Demangling` or `include/swift/Demangling` since the
last sync, so the vendored subset never rots silently.

The supported-version claim above is backed by per-toolchain corpus snapshots:
`scripts/collect-swift-corpus.sh` compiles a fixture with a concrete Swift
toolchain (pass its `swiftc` to represent another one) into
`tests/corpus/swift/<version>/`, and the `swift_corpus` test pins the exact
rendering. After a sync that changes output, regenerate snapshots deliberately
and review the diff — downstream consumers (blint) match on these strings:

```
MULTI_DEMANGLE_UPDATE_SNAPSHOTS=1 cargo test --all-features --test test_swift_corpus
```

## License

MIT

