Added 'cy exec' and 'py-exec' commands
authorMark Florisson <eggy.nospam@gmail.com>
Sat, 13 Nov 2010 00:21:57 +0000 (01:21 +0100)
committerMark Florisson <eggy.nospam@gmail.com>
Sat, 13 Nov 2010 00:21:57 +0000 (01:21 +0100)
Cython/Debugger/libcython.py
Cython/Debugger/libpython.py

index d54545b127de870da325c849d227fb264db53a51..dc4ccb991ce5c740f7b69592d58ca0651c53d225 100644 (file)
@@ -346,11 +346,7 @@ class CythonBase(object):
         
         selected_frame.select()
     
-    def get_cython_globals_dict(self):
-        """
-        Get the Cython globals dict where the remote names are turned into
-        local strings.
-        """
+    def get_remote_cython_globals_dict(self):
         m = gdb.parse_and_eval('__pyx_m')
         
         try:
@@ -361,7 +357,16 @@ class CythonBase(object):
                 with debugging support (-g)?"""))
             
         m = m.cast(PyModuleObject.pointer())
-        pyobject_dict = libpython.PyObjectPtr.from_pyobject_ptr(m['md_dict'])
+        return m['md_dict']
+        
+    
+    def get_cython_globals_dict(self):
+        """
+        Get the Cython globals dict where the remote names are turned into
+        local strings.
+        """
+        remote_dict = self.get_remote_cython_globals_dict()
+        pyobject_dict = libpython.PyObjectPtr.from_pyobject_ptr(remote_dict)
         
         result = {}
         seen = set()
@@ -598,6 +603,8 @@ class CyCy(CythonCommand):
             print_ = CyPrint.register(),
             locals = CyLocals.register(),
             globals = CyGlobals.register(),
+            exec_ = libpython.FixGdbCommand('cy exec', '-cy-exec'),
+            _exec = CyExec.register(),
             cy_cname = CyCName('cy_cname'),
             cy_cvalue = CyCValue('cy_cvalue'),
             cy_lineno = CyLine('cy_lineno'),
@@ -1092,6 +1099,75 @@ class CyGlobals(CyLocals):
                                            prefix='    ')
 
 
+class CyExec(CythonCommand):
+    name = '-cy-exec'
+    command_class = gdb.COMMAND_STACK
+    completer_class = gdb.COMPLETE_NONE
+    
+    def _fill_locals_dict(self, executor, local_dict_pointer):
+        "Fill a remotely allocated dict with values from the Cython C stack"
+        cython_func = self.get_cython_function()
+        
+        for name, cyvar in cython_func.locals.iteritems():
+            if cyvar.type == PythonObject:
+                # skip unitialized Cython variables 
+                try:
+                    val = gdb.parse_and_eval(cyvar.cname)
+                except RuntimeError:
+                    continue
+                else:
+                    # Fortunately, Cython initializes all local (automatic)
+                    # variables to NULL
+                    if libpython.pointervalue(val) == 0:
+                        continue
+
+                pystringp = executor.alloc_pystring(name)
+                code = '''
+                    PyDict_SetItem(
+                        (PyObject *) %d, 
+                        (PyObject *) %d, 
+                        (PyObject *) %s)
+                ''' % (local_dict_pointer, pystringp, cyvar.cname)
+                
+                # PyDict_SetItem doesn't steal our reference
+                executor.decref(pystringp)
+                
+                if gdb.parse_and_eval(code) < 0:
+                    gdb.parse_and_eval('PyErr_Print()')
+                    raise gdb.GdbError("Unable to execute Python code.")
+    
+    def _find_first_cython_or_python_frame(self):
+        frame = gdb.selected_frame()
+        while frame:
+            if (self.is_cython_function(frame) or 
+                self.is_python_function(frame)):
+                return frame
+            
+            frame = frame.older()
+        
+        raise gdb.GdbError("There is no Cython or Python frame on the stack.")
+        
+    def invoke(self, expr, from_tty):
+        frame = self._find_first_cython_or_python_frame()
+        if self.is_python_function(frame):
+            libpython.py_exec.invoke(expr, from_tty)
+            return
+        
+        executor = libpython.PythonCodeExecutor()
+        
+        # get the dict of Cython globals and construct a dict in the inferior
+        # with Cython locals
+        global_dict = gdb.parse_and_eval(
+            '(PyObject *) PyModule_GetDict(__pyx_m)')
+        local_dict = gdb.parse_and_eval('(PyObject *) PyDict_New()')
+        
+        try:
+            self._fill_locals_dict(executor, libpython.pointervalue(local_dict))
+            executor.evalcode(expr, global_dict, local_dict)
+        finally:
+            executor.decref(libpython.pointervalue(local_dict))
+
+
 # Functions
 
 class CyCName(gdb.Function, CythonBase):
index 7a5572e07386a6aebfb7ae518747940be8e6a1f2..c4173b647070075fb4e5480f7b2e0e81f375e377 100644 (file)
@@ -49,6 +49,7 @@ import os
 import re
 import sys
 import atexit
+import warnings
 import tempfile
 import itertools
 
@@ -1591,6 +1592,22 @@ gdb.execute = execute
 _logging_state = _LoggingState()
 
 
+def get_selected_inferior():
+    """
+    Return the selected inferior in gdb.
+    """
+    # Woooh, another bug in gdb! Is there an end in sight?
+    # http://sourceware.org/bugzilla/show_bug.cgi?id=12212
+    return gdb.inferiors()[0]
+    
+    selected_thread = gdb.selected_thread()
+    
+    for inferior in gdb.inferiors():
+        for thread in inferior.threads():
+            if thread == selected_thread:
+                return inferior
+
+
 class GenericCodeStepper(gdb.Command):
     """
     Superclass for code stepping. Subclasses must implement the following 
@@ -1687,7 +1704,7 @@ class GenericCodeStepper(gdb.Command):
         """
     
     def stopped(self):
-        return gdb.inferiors()[0].pid == 0
+        return get_selected_inferior().pid == 0
         
     def _stackdepth(self, frame):
         depth = 0
@@ -1881,4 +1898,192 @@ py_finish = PyFinish('py-finish')
 py_run = PyRun('py-run')
 py_cont = PyCont('py-cont')
 
-py_step.init_breakpoints()
\ No newline at end of file
+py_step.init_breakpoints()
+
+Py_single_input = 256
+Py_eval_input = 258
+
+def pointervalue(gdbval):
+    """
+    Return the value of the pionter as a Python int. 
+    
+    gdbval.type must be a pointer type
+    """
+    # don't convert with int() as it will raise a RuntimeError
+    if gdbval.address is not None:
+        return long(gdbval.address)
+    else:
+        # the address attribute is None sometimes, in which case we can
+        # still convert the pointer to an int
+        return long(gdbval)
+
+
+class PythonCodeExecutor(object):
+        
+    def malloc(self, size):
+        chunk = (gdb.parse_and_eval("(void *) malloc(%d)" % size))
+        
+        pointer = pointervalue(chunk)
+        if pointer == 0:
+            err("No memory could be allocated in the inferior.")
+        
+        return pointer
+        
+    def alloc_string(self, string):
+        pointer = self.malloc(len(string))
+        get_selected_inferior().write_memory(pointer, string)
+        
+        return pointer
+    
+    def alloc_pystring(self, string):
+        stringp = self.alloc_string(string)
+        try:
+            result = gdb.parse_and_eval(
+                'PyString_FromStringAndSize((char *) %d, (size_t) %d)' % 
+                                                 (stringp, len(string)))
+        finally:
+            self.free(stringp)
+        
+        pointer = pointervalue(result)
+        if pointer == 0:
+            err("Unable to allocate Python string in "
+                               "the inferior.")
+        
+        return pointer
+        
+    def free(self, pointer):
+        gdb.parse_and_eval("free((void *) %d)" % pointer)
+    
+    def decref(self, pointer):
+        "Decrement the reference count of a Python object in the inferior."
+        # Py_DecRef is like Py_XDECREF, but a function. So we don't have
+        # to check for NULL. This should also decref all our allocated
+        # Python strings.
+        gdb.parse_and_eval('Py_DecRef((PyObject *) %d)' % pointer)
+    
+    def evalcode(self, code, global_dict=None, local_dict=None):
+        """
+        Evaluate python code `code` given as a string in the inferior and
+        return the result as a gdb.Value. Returns a new reference in the
+        inferior.
+        
+        Of course, executing any code in the inferior may be dangerous and may
+        leave the debuggee in an unsafe state or terminate it alltogether.
+        """
+        if '\0' in code:
+            err("String contains NUL byte.")
+        
+        code += '\0'
+        
+        pointer = self.alloc_string(code)
+        
+        globalsp = pointervalue(global_dict)
+        localsp = pointervalue(local_dict)
+        
+        if globalp == 0 or localp == 0:
+            raise gdb.GdbError("Unable to obtain or create locals or globals.")
+        
+        code = """
+            PyRun_String(
+                (PyObject *) %(code)d,
+                (int) %(start)d,
+                (PyObject *) %(globals)s,
+                (PyObject *) %(locals)d)
+        """ % dict(code=pointer, start=Py_single_input, 
+                   globals=globalsp, locals=localsp)
+        
+        with FetchAndRestoreError():
+            try:
+                self.decref(gdb.parse_and_eval(code))
+            finally:
+                self.free(pointer)
+
+
+class FetchAndRestoreError(PythonCodeExecutor):
+    """
+    Context manager that fetches the error indicator in the inferior and
+    restores it on exit.
+    """
+
+    def __init__(self):
+        self.sizeof_PyObjectPtr = gdb.lookup_type('PyObject').pointer().sizeof
+        self.pointer = self.malloc(self.sizeof_PyObjectPtr * 3)
+        
+        type = self.pointer
+        value = self.pointer + self.sizeof_PyObjectPtr
+        traceback = self.pointer + self.sizeof_PyObjectPtr * 2
+        
+        self.errstate = type, value, traceback
+        
+    def __enter__(self):
+        gdb.parse_and_eval("PyErr_Fetch(%d, %d, %d)" % self.errstate)
+    
+    def __exit__(self, *args):
+        if gdb.parse_and_eval("(int) PyErr_Occurred()"):
+            gdb.parse_and_eval("PyErr_Print()")
+        
+        pyerr_restore = ("PyErr_Restore("
+                            "(PyObject *) *%d,"
+                            "(PyObject *) *%d,"
+                            "(PyObject *) *%d)")
+        
+        try:
+            gdb.parse_and_eval(pyerr_restore % self.errstate)
+        finally:
+            self.free(self.pointer)
+
+
+class FixGdbCommand(gdb.Command):
+
+    def __init__(self, command, actual_command):
+        super(FixGdbCommand, self).__init__(command, gdb.COMMAND_DATA,
+                                            gdb.COMPLETE_NONE)
+        self.actual_command = actual_command
+        
+    def fix_gdb(self):
+        """
+        So, you must be wondering what the story is this time! Yeeees, indeed, 
+        I have quite the story for you! It seems that invoking either 'cy exec'
+        and 'py-exec' work perfectly fine, but after this gdb's python API is 
+        entirely broken. Some unset exception value is still set? 
+        sys.exc_clear() didn't help. A demonstration:
+        
+        (gdb) cy exec 'hello'
+        'hello'
+        (gdb) python gdb.execute('cont')
+        RuntimeError: Cannot convert value to int.
+        Error while executing Python code.
+        (gdb) python gdb.execute('cont')
+        [15148 refs]
+    
+        Program exited normally.
+        """
+        warnings.filterwarnings('ignore', r'.*', RuntimeWarning, re.escape(__name__))
+        try:
+            long(gdb.parse_and_eval("(void *) 0")) == 0
+        except RuntimeError:
+            pass
+        # warnings.resetwarnings()
+    
+    def invoke(self, args, from_tty):
+        self.fix_gdb()
+        gdb.execute('%s %s' % (self.actual_command, args))
+        self.fix_gdb()
+
+
+class PyExec(gdb.Command):
+    
+    def invoke(self, expr, from_tty):
+        executor = PythonCodeExecutor()
+        global_dict = gdb.parse_and_eval('PyEval_GetGlobals()')
+        local_dict = gdb.parse_and_eval('PyEval_GetLocals()')
+        
+        if pointervalue(global_dict) == 0 or pointervalue(local_dict) == 0:
+            raise gdb.GdbError("Unable to find the locals or globals of the "
+                               "most recent Python function (relative to the "
+                               "selected frame).")
+        
+        executor.evalcode(expr, global_dict, local_dict)
+
+py_exec = FixGdbCommand('py-exec', '-py-exec')
+_py_exec = PyExec("-py-exec", gdb.COMMAND_DATA, gdb.COMPLETE_NONE)
\ No newline at end of file