Make Qt python pretty printers available inside LLDB/Xcode

The LLBD bridge can be imported from the users's ~/.lldbinit:

    command script import "<path to lldbbridge.py>"

Or by relying on a (future) debug script in the QtCore dSYM bundle.

Change-Id: Ia099dcebc6375d38ae2d75c939bb5669e30e4b2c
Reviewed-by: hjk <hjk@qt.io>
This commit is contained in:
Tor Arne Vestbø
2017-05-13 15:19:56 +02:00
committed by Tor Arne Vestbø
parent 4e35cc2ea8
commit 9b3ad89dcd
2 changed files with 431 additions and 29 deletions
+7 -2
View File
@@ -1,7 +1,7 @@
While the primary intention of this pretty printing implementation is
to provide what Qt Creator needs, it can be used in a plain commandline
GDB session, too.
to provide what Qt Creator needs, it can be used in a plain GDB and LLDB
session, too.
With
@@ -65,6 +65,11 @@ With code like
ss =
<QString> = {"Hello"}
Or for LLDB (.lldbinit or directly in the LLDB interpreter):
command script import <path/to/qtcreator>/share/qtcreator/debugger/lldbbridge.py
This will add LLDB summary providers for all the Qt types in a new type category named 'Qt'.
+424 -27
View File
@@ -31,8 +31,14 @@ import sys
import threading
import lldb
from contextlib import contextmanager
sys.path.insert(1, os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))))
# Simplify development of this module by reloading deps
if 'dumper' in sys.modules:
reload(sys.modules['dumper'])
from dumper import *
#######################################################################
@@ -52,7 +58,7 @@ def check(exp):
raise RuntimeError('Check failed')
class Dumper(DumperBase):
def __init__(self):
def __init__(self, debugger = None):
DumperBase.__init__(self)
lldb.theDumper = self
@@ -60,33 +66,38 @@ class Dumper(DumperBase):
self.typeCache = {}
self.outputLock = threading.Lock()
self.debugger = lldb.SBDebugger.Create()
#self.debugger.SetLoggingCallback(loggingCallback)
#def loggingCallback(args):
# s = args.strip()
# s = s.replace('"', "'")
# sys.stdout.write('log="%s"@\n' % s)
#Same as: self.debugger.HandleCommand('log enable lldb dyld step')
#self.debugger.EnableLog('lldb', ['dyld', 'step', 'process', 'state',
# 'thread', 'events',
# 'communication', 'unwind', 'commands'])
#self.debugger.EnableLog('lldb', ['all'])
self.debugger.Initialize()
self.debugger.HandleCommand('settings set auto-confirm on')
# FIXME: warn('DISABLING DEFAULT FORMATTERS')
# It doesn't work at all with 179.5 and we have some bad
# interaction in 300
# if not hasattr(lldb.SBType, 'GetCanonicalType'): # 'Test' for 179.5
#self.debugger.HandleCommand('type category delete gnu-libstdc++')
#self.debugger.HandleCommand('type category delete libcxx')
#self.debugger.HandleCommand('type category delete default')
self.debugger.DeleteCategory('gnu-libstdc++')
self.debugger.DeleteCategory('libcxx')
self.debugger.DeleteCategory('default')
self.debugger.DeleteCategory('cplusplus')
#for i in range(self.debugger.GetNumCategories()):
# self.debugger.GetCategoryAtIndex(i).SetEnabled(False)
if debugger:
# Re-use existing debugger
self.debugger = debugger
else:
self.debugger = lldb.SBDebugger.Create()
#self.debugger.SetLoggingCallback(loggingCallback)
#def loggingCallback(args):
# s = args.strip()
# s = s.replace('"', "'")
# sys.stdout.write('log="%s"@\n' % s)
#Same as: self.debugger.HandleCommand('log enable lldb dyld step')
#self.debugger.EnableLog('lldb', ['dyld', 'step', 'process', 'state',
# 'thread', 'events',
# 'communication', 'unwind', 'commands'])
#self.debugger.EnableLog('lldb', ['all'])
self.debugger.Initialize()
self.debugger.HandleCommand('settings set auto-confirm on')
# FIXME: warn('DISABLING DEFAULT FORMATTERS')
# It doesn't work at all with 179.5 and we have some bad
# interaction in 300
# if not hasattr(lldb.SBType, 'GetCanonicalType'): # 'Test' for 179.5
#self.debugger.HandleCommand('type category delete gnu-libstdc++')
#self.debugger.HandleCommand('type category delete libcxx')
#self.debugger.HandleCommand('type category delete default')
self.debugger.DeleteCategory('gnu-libstdc++')
self.debugger.DeleteCategory('libcxx')
self.debugger.DeleteCategory('default')
self.debugger.DeleteCategory('cplusplus')
#for i in range(self.debugger.GetNumCategories()):
# self.debugger.GetCategoryAtIndex(i).SetEnabled(False)
self.process = None
self.target = None
@@ -1770,3 +1781,389 @@ class Tester(Dumper):
warn('Cannot determined stopped thread')
lldb.SBDebugger.Destroy(self.debugger)
# ------------------------------ For use in LLDB ------------------------------
from pprint import pprint
__module__ = sys.modules[__name__]
DEBUG = False if not hasattr(__module__, 'DEBUG') else DEBUG
class LogMixin:
@staticmethod
def log(message = '', log_caller = False, frame = 1, args = ''):
if not DEBUG:
return
if log_caller:
message = ": " + message if len(message) else ''
# FIXME: Compute based on first frame not in this class?
frame = sys._getframe(frame)
fn = frame.f_code.co_name
localz = frame.f_locals
instance = str(localz["self"]) + "." if 'self' in localz else ''
message = "%s%s(%s)%s" % (instance, fn, args, message)
print message
@staticmethod
def log_fn(arg_str = ''):
LogMixin.log(log_caller = True, frame = 2, args = arg_str)
class DummyDebugger(object):
def __getattr__(self, attr):
raise AttributeError("Debugger should not be needed to create summaries")
class SummaryDumper(Dumper, LogMixin):
_instance = None
_lock = threading.RLock()
_type_caches = {}
@staticmethod
def initialize():
SummaryDumper._instance = SummaryDumper()
return SummaryDumper._instance
@staticmethod
@contextmanager
def shared(valobj):
SummaryDumper._lock.acquire()
dumper = SummaryDumper._instance
dumper.target = valobj.target
dumper.process = valobj.process
debugger_id = dumper.target.debugger.GetID()
dumper.typeCache = SummaryDumper._type_caches.get(debugger_id, {})
yield dumper
SummaryDumper._type_caches[debugger_id] = dumper.typeCache
SummaryDumper._lock.release()
@staticmethod
def warn(message):
print "Qt summary warning: %s" % message
@staticmethod
def showException(message, exType, exValue, exTraceback):
# FIXME: Store for later and report back in summary
SummaryDumper.log("Exception during dumping: %s" % exValue)
def __init__(self):
DumperBase.warn = staticmethod(SummaryDumper.warn)
DumperBase.showException = staticmethod(SummaryDumper.showException)
Dumper.__init__(self, DummyDebugger())
self.setVariableFetchingOptions({
'fancy' : True,
'qobjectnames' : True,
'passexceptions' : True
})
self.dumpermodules = ['qttypes']
self.loadDumpers({})
self.output = ''
def report(self, stuff):
return # Don't mess up lldb output
def dump_summary(self, valobj, expanded = False):
try:
from pygdbmi import gdbmiparser
except ImportError:
print "Qt summary provider requires the pygdbmi module, " \
"please install using 'sudo /usr/bin/easy_install pygdbmi', " \
"and then restart Xcode."
lldb.debugger.HandleCommand('type category delete Qt')
return None
value = self.fromNativeValue(valobj)
# Expand variable if we need synthetic children
oldExpanded = self.expandedINames
self.expandedINames = [value.name] if expanded else []
savedOutput = self.output
self.output = ''
with TopLevelItem(self, value.name):
self.putItem(value)
# FIXME: Hook into putField, etc to build up object instead of parsing MI
response = gdbmiparser.parse_response("^ok,summary=%s" % self.output)
self.output = savedOutput
self.expandedINames = oldExpanded
summary = response["payload"]["summary"]
#print value, " --> ",
#pprint(summary)
return summary
class SummaryProvider(LogMixin):
DEFAULT_SUMMARY = ''
VOID_PTR_TYPE = None
@staticmethod
def provide_summary(valobj, internal_dict, options = None):
if not __name__ in internal_dict:
# When disabling the import of the Qt summary providers, the
# summary functions are still registered with LLDB, and we will
# get callbacks to provide summaries, but at this point the child
# providers are not active, so instead of providing half-baked and
# confusing summaries we opt to unload all the summaries.
SummaryDumper.log("Module '%s' was unloaded, removing Qt category" % __name__)
lldb.debugger.HandleCommand('type category delete Qt')
return SummaryProvider.DEFAULT_SUMMARY
# FIXME: It would be nice to cache the providers, so that if a
# synthetic child provider has already been created for a valobj,
# we can re-use that when providing summary for the synthetic child
# parent, but we lack a consistent cache key for that to work.
provider = SummaryProvider(valobj)
provider.update()
return provider.get_summary(options)
def __init__(self, valobj, expand = False):
# Prevent recursive evaluation during logging, etc
valobj.SetPreferSyntheticValue(False)
self.log_fn(valobj.path)
self.valobj = valobj
self.expand = expand
if not SummaryProvider.VOID_PTR_TYPE:
with SummaryDumper.shared(self.valobj) as dumper:
SummaryProvider.VOID_PTR_TYPE = dumper.lookupNativeType('void*')
def __repr__(self):
return "%s(%s)" % (self.__class__.__name__, repr(self.key()))
def key(self):
if not hasattr(self, 'valobj'):
return None
return self.valobj.path
def update(self):
self.log_fn()
with SummaryDumper.shared(self.valobj) as dumper:
self.summary = dumper.dump_summary(self.valobj, self.expand)
def get_summary(self, options):
self.log_fn()
summary = self.summary
if not summary:
return '<error: could not get summary from Qt provider>'
encoding = summary.get("valueencoded")
summaryValue = summary["value"]
# FIXME: Share between Creator and LLDB in the python code
if encoding:
special_encodings = {
"empty" : "<empty>",
"minimumitemcount" : "<at least %s items>" % summaryValue,
"undefined" : "<undefined>",
"null" : "<null>",
"itemcount" : "<%s items>" % summaryValue,
"notaccessible" : "<not accessible>",
"optimizedout" : "<optimized out>",
"nullreference" : "<null reference>",
"emptystructure" : "<empty structure>",
"uninitialized" : "<uninitialized>",
"invalid" : "<invalid>",
"notcallable" : "<not callable>",
"outofscope" : "<out of scope>"
}
if encoding in special_encodings:
return special_encodings[encoding]
text_encodings = [
'latin1',
# 'local8bit',
'utf8',
'utf16',
# 'ucs4'
]
if encoding in text_encodings:
try:
binaryvalue = summaryValue.decode('hex')
# LLDB expects UTF-8
return "\"%s\"" % (binaryvalue.decode(encoding).encode('utf8'))
except:
return "<failed to decode '%s' as '%s'>" % (summaryValue, encoding)
# FIXME: Support these
other_encodings = ['int', 'uint', 'float', 'juliandate', 'juliandateandmillisecondssincemidnight',
'millisecondssincemidnight', 'ipv6addressandhexscopeid', 'datetimeinternal']
summaryValue += " <encoding='%s'>" % encoding
if self.valobj.value:
# If we've resolved a pointer that matches what LLDB natively chose,
# then use that instead of printing the values twice. FIXME, ideally
# we'd have the same pointer format as LLDB uses natively.
if re.sub('^0x0*', '', self.valobj.value) == re.sub('^0x0*', '', summaryValue):
return SummaryProvider.DEFAULT_SUMMARY
return summaryValue.strip()
class SyntheticChildrenProvider(SummaryProvider):
def __init__(self, valobj, dict):
SummaryProvider.__init__(self, valobj, expand = True)
self.summary = None
self.synthetic_children = []
def num_native_children(self):
return self.valobj.GetNumChildren()
def num_children(self):
self.log("native: %d synthetic: %d" %
(self.num_native_children(), len(self.synthetic_children)), True)
return self._num_children()
def _num_children(self):
return self.num_native_children() + len(self.synthetic_children)
def has_children(self):
return self._num_children() > 0
def get_child_index(self, name):
# FIXME: Do we ever need this to return something?
self.log_fn(name)
return None
def get_child_at_index(self, index):
self.log_fn(index)
if index < 0 or index > self._num_children():
return None
if not self.valobj.IsValid() or not self.summary:
return None
if index < self.num_native_children():
# Built-in children
value = self.valobj.GetChildAtIndex(index)
else:
# Synthetic children
index -= self.num_native_children()
child = self.synthetic_children[index]
name = child.get('name', "[%s]" % index)
value = self.create_value(child, name)
return value
def create_value(self, child, name = ''):
child_type = child.get('type', self.summary.get('childtype'))
value = None
if child_type:
if 'address' in child:
value_type = None
with SummaryDumper.shared(self.valobj) as dumper:
value_type = dumper.lookupNativeType(child_type)
if not value_type or not value_type.IsValid():
return None
address = int(child['address'], 16)
# Create as void* so that the value is fully initialized before
# we trigger our own summary/child providers recursively.
value = self.valobj.synthetic_child_from_address(name, address,
SummaryProvider.VOID_PTR_TYPE).Cast(value_type)
else:
self.log("Don't know how to create value for child %s" % child)
# FIXME: Figure out if we ever will hit this case, and deal with it
#value = self.valobj.synthetic_child_from_expression(name,
# "(%s)(%s)" % (child_type, child['value']))
else:
# FIXME: Find a good way to deal with synthetic values
self.log("Don't know how to create value for child %s" % child)
# FIXME: Handle value type or value errors consistently
return value
def update(self):
SummaryProvider.update(self)
self.synthetic_children = []
if not 'children' in self.summary:
return
dereference_child = None
for child in self.summary['children']:
if not child or not type(child) is dict:
continue
# Skip base classes, they are built-in children
# FIXME: Is there a better check than 'iname'?
if 'iname' in child:
continue
name = child.get('name')
if name:
if name == '*':
# No use for this unless the built in children failed to resolve
dereference_child = child
continue
if name.startswith('['):
# Skip pure synthetic children until we can deal with them
continue
if name.startswith('#'):
# Skip anonymous unions, lookupNativeType doesn't handle them (yet),
# so it triggers the slow lookup path, and the union is provided as
# a native child anyways.
continue
# Skip native children
if self.valobj.GetChildMemberWithName(name):
continue
self.synthetic_children.append(child)
# Xcode will sometimes fail to show the children of pointers in
# the debugger UI, even if dereferencing the pointer works fine.
# We fall back to the special child reported by the Qt dumper.
if not self.valobj.GetNumChildren() and dereference_child:
self.valobj = self.create_value(dereference_child)
self.update()
def __lldb_init_module(debugger, internal_dict):
# Module is being imported in an LLDB session
dumper = SummaryDumper.initialize()
type_category = 'Qt'
# Concrete types
summary_function = "%s.%s.%s" % (__name__, 'SummaryProvider', 'provide_summary')
types = map(lambda x: x.replace('__', '::'), dumper.qqDumpers)
debugger.HandleCommand("type summary add -F %s -w %s %s" \
% (summary_function, type_category, ' '.join(types)))
regex_types = map(lambda x: "'" + x + "'", dumper.qqDumpersEx.keys())
if regex_types:
debugger.HandleCommand("type summary add -x -F %s -w %s %s" \
% (summary_function, type_category, ' '.join(regex_types)))
# Global catch-all for Qt classes
debugger.HandleCommand("type summary add -x '^Q.*$' -F %s -w %s" \
% (summary_function, type_category))
# Named summary ('frame variable foo --summary Qt')
debugger.HandleCommand("type summary add --name Qt -F %s -w %s" \
% (summary_function, type_category))
# Synthetic children
debugger.HandleCommand("type synthetic add -x '^Q.*$' -l %s -w %s" \
% (SyntheticChildrenProvider, type_category))
debugger.HandleCommand('type category enable %s' % type_category)
if not __name__ == 'qt':
# Make available under global 'qt' name for consistency
internal_dict['qt'] = internal_dict[__name__]