Created
September 3, 2023 20:51
-
-
Save brennanMKE/de48237ce5ffa33a4c6144eece231f51 to your computer and use it in GitHub Desktop.
Demangle and log call static symbols during runtime
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Revised code written by Naruki Chigira under MIT license. | |
// https://github.com/naru-jpn/CallStackSymbols | |
// See: https://stackoverflow.com/questions/24321773/how-can-i-demangle-a-swift-class-name-dynamically | |
// Darwin/Swift: https://github.com/apple/swift/blob/main/stdlib/public/runtime/Demangle.cpp#L913 | |
#if DEBUG | |
import Foundation | |
import Darwin | |
import os.log | |
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "CallStackSymbols") | |
func debugCallStack(tag: String = "-") { | |
let callStackSymbols = Thread.callStackSymbols.dropFirst() | |
let lines = callStackSymbols.compactMap { try? CallStackLine.parse(line: $0).demangledLine } | |
logger.debug("[\(tag)] * Start Call Stack Symbols *") | |
for line in lines { | |
logger.debug("\(line)") | |
} | |
logger.debug("[\(tag)] * End Call Stack Symbols *") | |
} | |
typealias Swift_Demangle = @convention(c) (_ mangledName: UnsafePointer<UInt8>?, | |
_ mangledNameLength: Int, | |
_ outputBuffer: UnsafeMutablePointer<UInt8>?, | |
_ outputBufferSize: UnsafeMutablePointer<Int>?, | |
_ flags: UInt32) -> UnsafeMutablePointer<Int8>? | |
// Warning: The swift_demangle is an internal function which could change at any time. Use for development only. | |
public func swift_demangle(_ mangled: String) -> String? { | |
let RTLD_DEFAULT = dlopen(nil, RTLD_NOW) | |
if let sym = dlsym(RTLD_DEFAULT, "swift_demangle") { | |
let demangle = unsafeBitCast(sym, to: Swift_Demangle.self) | |
if let cString = demangle(mangled, mangled.count, nil, nil, 0) { | |
defer { cString.deallocate() } | |
return String(cString: cString) | |
} | |
} | |
return nil | |
} | |
public struct CallStackLine { | |
public enum Failure: Error { | |
case failedToParse(reason: String) | |
} | |
/// Depth of the call. | |
public let depth: Int | |
/// The pathname of the shared object (dynamic library) that contains the address. | |
public let fname: String | |
/// The base address at which the shared object is loaded into memory. | |
public let fbase: UInt64 | |
/// The name of the symbol (if found) that is closest to the address, but not greater than the address. | |
public let sname: String // symbol | |
/// The exact address of the symbol found. | |
public let saddr: UInt64 | |
/// Original line. | |
public let line: String | |
public var demangled: String { | |
swift_demangle(sname) ?? sname | |
} | |
public var demangledLine: String { | |
guard let demangled = swift_demangle(sname) else { | |
return line | |
} | |
// replace sname with the demangled value | |
return line.replacingOccurrences(of: sname, with: demangled) | |
} | |
public static func parse(line: String) throws -> CallStackLine { | |
var remaining = line | |
let depth: Int | |
do { | |
let regex = try NSRegularExpression(pattern: "^[\\d]+") | |
guard let match = regex.firstMatch(in: remaining, range: .init(location: 0, length: remaining.count)) else { | |
throw Failure.failedToParse(reason: "Cannot find depth") | |
} | |
let start = remaining.index(remaining.startIndex, offsetBy: match.range.location) | |
let end = remaining.index(start, offsetBy: match.range.length) | |
guard let number = Int(remaining[(start)..<(end)]) else { | |
throw Failure.failedToParse(reason: "Cannot convert depth string to integer") | |
} | |
depth = number | |
remaining = String(remaining.dropFirst(match.range.length)).trimmingCharacters(in: .whitespaces) | |
} catch { | |
throw Failure.failedToParse(reason: "Cannot get depth") | |
} | |
guard let fname = remaining.components(separatedBy: .whitespaces).first else { | |
throw Failure.failedToParse(reason: "Cannot get fname") | |
} | |
remaining = String(remaining.dropFirst(fname.count)).trimmingCharacters(in: .whitespaces) | |
guard let fbaseString = remaining.components(separatedBy: .whitespaces).first else { | |
throw Failure.failedToParse(reason: "Cannot get fbase") | |
} | |
guard fbaseString.hasPrefix("0x") else { | |
throw Failure.failedToParse(reason: "Invalid fbase format (fbase should be '0x...')") | |
} | |
guard let fbase = UInt64(fbaseString.dropFirst(2), radix: 16) else { | |
throw Failure.failedToParse(reason: "Cannot convert fbase string to integer") | |
} | |
remaining = String(remaining.dropFirst(fbaseString.count)).trimmingCharacters(in: .whitespaces) | |
let saddr: UInt64 | |
do { | |
let regex = try NSRegularExpression(pattern: "[\\d]+$") | |
guard let match = regex.firstMatch(in: remaining, range: .init(location: 0, length: remaining.count)) else { | |
throw Failure.failedToParse(reason: "Cannot find saddr") | |
} | |
let start = remaining.index(remaining.startIndex, offsetBy: match.range.location) | |
let end = remaining.index(start, offsetBy: match.range.length) | |
guard let number = UInt64(remaining[(start)..<(end)]) else { | |
throw Failure.failedToParse(reason: "Cannot convert depth string to integer") | |
} | |
saddr = number | |
remaining = String(remaining.dropLast(match.range.length + 3)).trimmingCharacters(in: .whitespaces) | |
} catch { | |
throw Failure.failedToParse(reason: "Cannot get saddr") | |
} | |
let sname = remaining | |
return CallStackLine(depth: depth, fname: fname, fbase: fbase, sname: sname, saddr: saddr, line: line) | |
} | |
} | |
#endif |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment