Skip to content

Instantly share code, notes, and snippets.

@brennanMKE
Created September 3, 2023 20:51
Show Gist options
  • Save brennanMKE/de48237ce5ffa33a4c6144eece231f51 to your computer and use it in GitHub Desktop.
Save brennanMKE/de48237ce5ffa33a4c6144eece231f51 to your computer and use it in GitHub Desktop.
Demangle and log call static symbols during runtime
// 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