forked from qt-creator/qt-creator
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:
committed by
Tor Arne Vestbø
parent
4e35cc2ea8
commit
9b3ad89dcd
@@ -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'.
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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__]
|
||||
|
||||
Reference in New Issue
Block a user