summaryrefslogtreecommitdiff
path: root/share
diff options
context:
space:
mode:
authorTor Arne Vestbø <torarnv@gmail.com>2017-05-13 15:19:56 +0200
committerTor Arne Vestbø <tor.arne.vestbo@qt.io>2017-05-19 14:03:46 +0000
commit9b3ad89dcd7399c59a178f2d66b924432c999985 (patch)
tree04d77d60dfbb1e3c83046738c65ea5cbac6224c9 /share
parent4e35cc2ea80effe757ff0c81a739aa1eb761b7c4 (diff)
downloadqt-creator-9b3ad89dcd7399c59a178f2d66b924432c999985.tar.gz
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>
Diffstat (limited to 'share')
-rw-r--r--share/qtcreator/debugger/README.txt9
-rw-r--r--share/qtcreator/debugger/lldbbridge.py453
2 files changed, 432 insertions, 30 deletions
diff --git a/share/qtcreator/debugger/README.txt b/share/qtcreator/debugger/README.txt
index 8307da3c8d..0913f39f50 100644
--- a/share/qtcreator/debugger/README.txt
+++ b/share/qtcreator/debugger/README.txt
@@ -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'.
diff --git a/share/qtcreator/debugger/lldbbridge.py b/share/qtcreator/debugger/lldbbridge.py
index 8287d5e8f9..86a0d3e9bc 100644
--- a/share/qtcreator/debugger/lldbbridge.py
+++ b/share/qtcreator/debugger/lldbbridge.py
@@ -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__]