Skip to content

Commit

Permalink
Add support for Rust v0 symbol mangling scheme (#491)
Browse files Browse the repository at this point in the history
  • Loading branch information
cerisier authored Jan 16, 2025
1 parent c04a148 commit dd989ea
Show file tree
Hide file tree
Showing 15 changed files with 226 additions and 54 deletions.
13 changes: 0 additions & 13 deletions src/lib/demangle-cpp.test.ts

This file was deleted.

32 changes: 0 additions & 32 deletions src/lib/demangle-cpp.ts

This file was deleted.

1 change: 1 addition & 0 deletions src/lib/demangle/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
gcc
39 changes: 39 additions & 0 deletions src/lib/demangle/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
EMCC = emcc
CFLAGS = -Os -Igcc/include -DHAVE_STDLIB_H -DHAVE_STRING_H
LDFLAGS_COMMON = \
-s EXPORTED_RUNTIME_METHODS=stringToUTF8OnStack,UTF8ToString \
-s EXPORTED_FUNCTIONS=_demangle,_free \
-s MODULARIZE=1 \
-s WASM=1 \
-s FILESYSTEM=0 \
-s MINIMAL_RUNTIME=1

# We have to disable EXPORT_ES6 as otherwise since esbuild cannot transpile that
# into umd or anything else.
# .ts files throughout the project can do this just fine because they are
# transpiled to compatible js.
ifeq ($(TEST),1)
LDFLAGS = $(LDFLAGS_COMMON) -s EXPORT_ES6=1
else
# SINGLE_FILE=1 embeds the wasm as base64.
LDFLAGS = $(LDFLAGS_COMMON) -s ASSERTIONS=0 -s SINGLE_FILE=1 -s ENVIRONMENT=web
endif

SRC_FILES = \
gcc/libiberty/safe-ctype.c \
gcc/libiberty/rust-demangle.c \
gcc/libiberty/cp-demangle.c \
demangle.c
POST_JS = demangle.post.js
EXTERN_POST_JS = demangle.extern.post.js
OUTPUT = demangle.wasm.js

all: $(OUTPUT)

$(OUTPUT): $(SRC_FILES) $(POST_JS)
$(EMCC) $(CFLAGS) $(SRC_FILES) $(LDFLAGS) --post-js $(POST_JS) --extern-post-js $(EXTERN_POST_JS) --no-entry -o $@

clean:
rm -f $(OUTPUT)

.PHONY: all clean
30 changes: 30 additions & 0 deletions src/lib/demangle/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# demangle

A wrapper function on top of demangling functions from the `GNU libiberty`,
using emscripten.

# Build dependencies

## emscripten 4.0.0

Follow the official `emsdk` installation instructions:

https://emscripten.org/docs/getting_started/downloads.html#installation-instructions-using-the-emsdk-recommended

And make sure you have `emcc` in your PATH.

# Source dependencies

## GCC

Make sure to fetch `gcc` sources.

* `git clone https://github.com/gcc-mirror/gcc`
* `git reset --hard 40754a3b9bef83bf4da0675fcb378e8cd1675602`

# Build instructions

`make` to produce a single CommonJS module that contains also contain the base64 encoded wasm file.
`make TEST=1` to produce both a ES6 module AND the wasm file.

Using `make TEST=1` produce a file that can be used by `node` for testing purposes.
38 changes: 38 additions & 0 deletions src/lib/demangle/demangle.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#include "gcc/include/demangle.h"

#include <string.h>

static char *non_microsoft_demangle(const char *mangled) {
int is_itanium_symbol = strncmp(mangled, "_Z", 2) == 0;
if (is_itanium_symbol) {
// Note: __cxa_demangle default is DMGL_PARAMS | DMGL_TYPES
return cplus_demangle_v3(mangled, DMGL_PARAMS | DMGL_TYPES);
}

int is_rust_symbol = strncmp(mangled, "_R", 2) == 0;
if (is_rust_symbol) {
// Note: rust_demangle uses only DMGL_VERBOSE and DMGL_NO_RECURSE_LIMIT,
// so no need to pass any options in our case.
return rust_demangle(mangled, DMGL_NO_OPTS);
}

return NULL;
}

// Logic is inspired by llvm::demangle.
// It is the caller's responsibility to free the string which is returned.
char *demangle(const char *mangled) {
char *demangled = non_microsoft_demangle(mangled);
if (demangled) {
return demangled;
}

if (mangled[0] == '_') {
demangled = non_microsoft_demangle(&mangled[1]);
if (demangled) {
return demangled;
}
}

return NULL;
}
7 changes: 7 additions & 0 deletions src/lib/demangle/demangle.extern.post.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* DO NOT USE THIS FILE DIRECTLY.
*
* This file is only used as --extern-post-js of emcc.
*/
module.exports = Module
module.exports.default = Module
22 changes: 22 additions & 0 deletions src/lib/demangle/demangle.post.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* DO NOT USE THIS FILE DIRECTLY.
*
* This file is only used as --post-js of emcc.
*
* This file provides a higher level demangle function ready to use
* in JavaScript.
*/
Module['wasm_demangle'] = function(mangled) {
/*
* We are manually calling the lower-level generated functions
* instead of using `cwrap` because we need to `free` the pointer
* returned by `_demangle`.
*/
const param_ptr = stringToUTF8OnStack(mangled);
const result_ptr = _demangle(param_ptr);
const result = UTF8ToString(result_ptr);
if (result_ptr !== null && result_ptr !== undefined) {
_free(result_ptr);
}
return result;
}
28 changes: 28 additions & 0 deletions src/lib/demangle/demangle.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import {loadDemangling} from './demangle'

test('demangle', async () => {
const demangle = await loadDemangling()

expect(demangle('a')).toBe('a')
expect(demangle('someUnobfuscatedFunction')).toBe('someUnobfuscatedFunction')

// C++ mangling
expect(demangle('__ZNK7Support6ColorFeqERKS0_')).toBe(
'Support::ColorF::operator==(Support::ColorF const&) const',
)
// Running a second time to test the cache
expect(demangle('__ZNK7Support6ColorFeqERKS0_')).toBe(
'Support::ColorF::operator==(Support::ColorF const&) const',
)

// Rust v0 mangling
expect(demangle('_RNvCskwGfYPst2Cb_3foo16example_function')).toBe('foo::example_function')

// Rust legacy mangling
expect(demangle('_ZN3std2fs8Metadata7created17h8df207f105c5d474E')).toBe(
'std::fs::Metadata::created::h8df207f105c5d474',
)

// False positive
expect(demangle('_ZoomIn')).toBe('_ZoomIn')
})
27 changes: 27 additions & 0 deletions src/lib/demangle/demangle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import createWasmDemangleModule from './demangle.wasm'

const wasmDemangleModulePromise = createWasmDemangleModule().then(module => module)

const cache = new Map<string, string>()

export async function loadDemangling(): Promise<(name: string) => string> {
// This function converts a mangled C++ name such as "__ZNK7Support6ColorFeqERKS0_"
// into a human-readable symbol (in this case "Support::ColorF::==(Support::ColorF&)")
const wasmDemangleModule = await wasmDemangleModulePromise
return cached(wasmDemangleModule.wasm_demangle)
}

function cached(demangle: (name: string) => string): (name: string) => string {
return (name: string): string => {
let result = cache.get(name)
if (result !== undefined) {
name = result
} else {
result = demangle(name)
result = result === '' ? name : result
cache.set(name, result)
name = result
}
return name
}
}
5 changes: 5 additions & 0 deletions src/lib/demangle/demangle.wasm.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
interface WasmDemangleModule {
wasm_demangle(mangled: string): string
}

export default function ModuleFactory(options?: unknown): Promise<WasmDemangleModule>
16 changes: 16 additions & 0 deletions src/lib/demangle/demangle.wasm.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/lib/demangle/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {loadDemangling} from './demangle'
19 changes: 11 additions & 8 deletions src/lib/profile.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import {lastOf, KeyedSet} from './utils'
import {ValueFormatter, RawValueFormatter} from './value-formatters'
import {FileFormat} from './file-format-spec'
const demangleCppModule = import('./demangle-cpp')

export interface FrameInfo {
key: string | number
Expand Down Expand Up @@ -404,16 +403,20 @@ export class Profile {

// Demangle symbols for readability
async demangle() {
let demangleCpp: ((name: string) => string) | null = null
let demangle: ((name: string) => string) | null = null

for (let frame of this.frames) {
// This function converts a mangled C++ name such as "__ZNK7Support6ColorFeqERKS0_"
// into a human-readable symbol (in this case "Support::ColorF::==(Support::ColorF&)")
if (frame.name.startsWith('__Z')) {
if (!demangleCpp) {
demangleCpp = (await demangleCppModule).demangleCpp
// This function converts a mangled C++ and Rust name into a human-readable symbol.
if (
frame.name.startsWith('__Z') ||
frame.name.startsWith('_R') ||
frame.name.startsWith('_Z')
) {
if (!demangle) {
const demangleModule = await import('./demangle')
demangle = await demangleModule.loadDemangling()
}
frame.name = demangleCpp(frame.name)
frame.name = demangle(frame.name)
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/views/application.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const importModule = import('../import')
// We put them all in one place so we can directly control the relative priority
// of these.
importModule.then(() => {})
import('../lib/demangle-cpp').then(() => {})
import('../lib/demangle').then(() => {})
import('source-map').then(() => {})

async function importProfilesFromText(
Expand Down

0 comments on commit dd989ea

Please sign in to comment.