Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 71 additions & 1 deletion Doc/c-api/frame.rst
Original file line number Diff line number Diff line change
Expand Up @@ -226,11 +226,31 @@ Unless using :pep:`523`, you will not need this.

.. c:function:: PyObject* PyUnstable_InterpreterFrame_GetCode(struct _PyInterpreterFrame *frame);

Return a :term:`strong reference` to the code object for the frame.
Return a :term:`strong reference` to the code object for the frame.
Does not raise an exception.

If allocation and reference count changes are not permitted (for example,
from a signal handler or a custom memory allocator), use
:c:func:`PyUnstable_InterpreterFrame_GetCodeSafe` instead.

.. versionadded:: 3.12


.. c:function:: PyObject* PyUnstable_InterpreterFrame_GetCodeSafe(struct _PyInterpreterFrame *frame);

Return a :term:`borrowed reference` to the code object for the frame.
The reference is valid as long as the frame is alive.

Use this instead of :c:func:`PyUnstable_InterpreterFrame_GetCode` when
allocation and reference count changes are not permitted (for example,
from a signal handler or a custom memory allocator). Does not allocate
memory, does not change any reference counts, does not acquire or release
the GIL, and does not raise an exception. Uses heuristics to detect freed
memory — not 100% reliable in the presence of concurrent deallocation.

.. versionadded:: 3.15


.. c:function:: int PyUnstable_InterpreterFrame_GetLasti(struct _PyInterpreterFrame *frame);

Return the byte offset into the last executed instruction.
Expand All @@ -243,3 +263,53 @@ Unless using :pep:`523`, you will not need this.
Return the currently executing line number, or -1 if there is no line number.

.. versionadded:: 3.12


.. c:function:: int PyUnstable_InterpreterFrame_GetLineSafe(struct _PyInterpreterFrame *frame)

Return the currently executing line number, or ``-1`` if there is no line
number or the frame is invalid. Does not raise an exception.

Unlike :c:func:`PyUnstable_InterpreterFrame_GetLine`, validates the code
object and instruction offset before accessing the line table rather than
asserting them, making it safe to call when the frame state may be
partially torn down.

.. versionadded:: 3.15


.. c:function:: struct _PyInterpreterFrame* PyUnstable_ThreadState_GetInterpreterFrame(PyThreadState *tstate)

Return the innermost complete interpreter frame of *tstate*, or ``NULL`` if
the thread has no complete frame or freed memory is detected. Incomplete
frames (interpreter entry trampolines and frames that have not yet begun
executing) are skipped automatically.

Does not allocate memory, does not raise an exception, and does not acquire
or release the GIL. Safe to call from a signal handler; racy reads from
other threads are intentional. Uses heuristics to detect freed memory —
not 100% reliable in the presence of concurrent deallocation.

To iterate over the full call stack, call
:c:func:`PyUnstable_InterpreterFrame_GetNextComplete` repeatedly on the
returned frame until it returns ``NULL``.

.. versionadded:: 3.15


.. c:function:: struct _PyInterpreterFrame* PyUnstable_InterpreterFrame_GetNextComplete(struct _PyInterpreterFrame *frame)

Return the next (calling) complete frame, or ``NULL`` if *frame* is the
outermost complete frame or freed memory is detected. Incomplete frames are
skipped automatically.

Does not allocate memory, does not raise an exception, and does not acquire
or release the GIL. Safe to call from a signal handler; racy reads from
other threads are intentional. Uses heuristics to detect freed memory —
not 100% reliable in the presence of concurrent deallocation.

Unlike :c:func:`PyFrame_GetBack`, this function never allocates memory,
making it safe to call from a custom memory allocator hook without risking
re-entrant allocation.

.. versionadded:: 3.15
43 changes: 42 additions & 1 deletion Include/cpython/pyframe.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,20 @@ PyAPI_FUNC(PyObject*) PyFrame_GetVarString(PyFrameObject *frame, const char *nam
struct _PyInterpreterFrame;

/* Returns the code object of the frame (strong reference).
* Does not raise an exception. */
* Does not raise an exception.
* If allocation and reference count changes are not permitted, use
* PyUnstable_InterpreterFrame_GetCodeSafe instead. */
PyAPI_FUNC(PyObject *) PyUnstable_InterpreterFrame_GetCode(struct _PyInterpreterFrame *frame);

/* Returns the code object of the frame as a borrowed reference.
* The reference is valid as long as the frame is alive.
* Use instead of PyUnstable_InterpreterFrame_GetCode when allocation and
* reference count changes are not permitted (e.g. from a signal handler or
* a custom memory allocator). Does not allocate, does not change any
* reference counts, does not acquire or release the GIL, does not raise an
* exception. Uses heuristics to detect freed memory; not 100% reliable. */
PyAPI_FUNC(PyObject *) PyUnstable_InterpreterFrame_GetCodeSafe(struct _PyInterpreterFrame *frame);

/* Returns a byte offset into the last executed instruction.
* Does not raise an exception. */
PyAPI_FUNC(int) PyUnstable_InterpreterFrame_GetLasti(struct _PyInterpreterFrame *frame);
Expand All @@ -36,6 +47,36 @@ PyAPI_FUNC(int) PyUnstable_InterpreterFrame_GetLasti(struct _PyInterpreterFrame
* Does not raise an exception. */
PyAPI_FUNC(int) PyUnstable_InterpreterFrame_GetLine(struct _PyInterpreterFrame *frame);

/* Returns the currently executing line number, or -1 if there is no line
* number or the frame is invalid.
* Unlike PyUnstable_InterpreterFrame_GetLine, validates the code object and
* instruction offset before accessing the line table rather than asserting
* them, making it safe to call when the frame state may be partially torn
* down. Does not raise an exception. */
PyAPI_FUNC(int) PyUnstable_InterpreterFrame_GetLineSafe(struct _PyInterpreterFrame *frame);


/* Returns the innermost complete interpreter frame of the thread state, or
* NULL if the thread has no complete frame or freed memory is detected.
* Skips over incomplete frames (interpreter entry trampolines and frames that
* have not yet begun executing) automatically.
* Does not allocate memory, does not acquire or release the GIL, does not
* raise an exception. Safe to call from signal handlers; racy reads from
* other threads are intentional and suppressed (_Py_NO_SANITIZE_THREAD).
* Uses heuristics to detect freed memory; not 100% reliable. */
PyAPI_FUNC(struct _PyInterpreterFrame *)
PyUnstable_ThreadState_GetInterpreterFrame(PyThreadState *tstate);

/* Returns the next (calling) complete frame, or NULL if frame is the
* outermost complete frame or freed memory is detected.
* Skips over incomplete frames automatically.
* Does not allocate memory, does not acquire or release the GIL, does not
* raise an exception. Safe to call from signal handlers; racy reads from
* other threads are intentional and suppressed (_Py_NO_SANITIZE_THREAD).
* Uses heuristics to detect freed memory; not 100% reliable. */
PyAPI_FUNC(struct _PyInterpreterFrame *)
PyUnstable_InterpreterFrame_GetNextComplete(struct _PyInterpreterFrame *frame);

#define PyUnstable_EXECUTABLE_KIND_SKIP 0
#define PyUnstable_EXECUTABLE_KIND_PY_FUNCTION 1
#define PyUnstable_EXECUTABLE_KIND_BUILTIN_FUNCTION 3
Expand Down
80 changes: 80 additions & 0 deletions Lib/test/test_capi/test_misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -2789,6 +2789,86 @@ def test_line(self):
firstline = self.func.__code__.co_firstlineno
self.assertEqual(line, firstline + 2)

def test_tstate_getframe_is_current(self):
# PyUnstable_ThreadState_GetInterpreterFrame must return the same
# PyFrameObject as sys._getframe(0) when called at the same level.
frame_from_c = _testinternalcapi.tstate_getframe()
self.assertIs(frame_from_c, sys._getframe(0))

def test_getnextcomplete_matches_f_back(self):
# GetNextComplete must match f_back at every step, all the way to None.
c_frame = _testinternalcapi.tstate_getframe()
py_frame = sys._getframe(0)
while c_frame is not None:
self.assertIs(c_frame, py_frame)
c_frame = _testinternalcapi.iframe_getnextcomplete(c_frame)
py_frame = py_frame.f_back
self.assertIsNone(py_frame)

def test_stack_to_yaml(self):
# stack_to_yaml uses only signal-safe operations for the walk and
# emission (no allocation, no refcount changes, no GIL release).
# Verify the output is well-formed and contains the expected frames.
def inner():
return _testinternalcapi.stack_to_yaml()

yaml_str = inner()
self.assertIsInstance(yaml_str, str)

# Parse the YAML manually to avoid a PyYAML dependency.
# Format is blocks of:
# - filename: <path>
# name: <name>
# lineno: <n>
frames = []
current = {}
for line in yaml_str.splitlines():
if line.startswith('- filename: '):
if current:
frames.append(current)
current = {'filename': line[len('- filename: '):]}
elif line.startswith(' name: '):
current['name'] = line[len(' name: '):]
elif line.startswith(' lineno: '):
current['lineno'] = int(line[len(' lineno: '):])
if current:
frames.append(current)

self.assertGreater(len(frames), 1)
# Innermost frame is the Python function that called stack_to_yaml.
self.assertEqual(frames[0]['name'], 'inner')
# Next frame is this test method.
self.assertEqual(frames[1]['name'], 'test_stack_to_yaml')
# Every frame must have a non-empty filename and a valid lineno.
for f in frames:
self.assertTrue(f.get('filename'), f)
self.assertIsInstance(f.get('lineno'), int, f)

def test_getcodesafe_matches_fcode(self):
# GetCodeSafe must return the same code object as frame.f_code.
frame = _testinternalcapi.tstate_getframe()
self.assertIs(_testinternalcapi.iframe_getcodesafe(frame), frame.f_code)


def test_iframe_getlinesafe(self):
# Use a generator frame frozen at a yield point so that iframe_getlasti
# and iframe_getlinesafe both read the same (stable) instruction pointer.
def gen():
yield
g = gen()
next(g)
frame = g.gi_frame
lasti = _testinternalcapi.iframe_getlasti(frame)
lineno = _testinternalcapi.iframe_getlinesafe(frame)
if lasti >= 0:
expected = None
for start, end, ln in frame.f_code.co_lines():
if ln is not None and start <= lasti < end:
expected = ln
break
if expected is not None:
self.assertEqual(lineno, expected)


SUFFICIENT_TO_DEOPT_AND_SPECIALIZE = 100

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Add four new ``PyUnstable_*`` C API functions for signal-safe, allocation-free
call-stack iteration: :c:func:`PyUnstable_ThreadState_GetInterpreterFrame`,
:c:func:`PyUnstable_InterpreterFrame_GetNextComplete`,
:c:func:`PyUnstable_InterpreterFrame_GetCodeSafe`, and
:c:func:`PyUnstable_InterpreterFrame_GetLineSafe`. These functions do not
allocate memory, do not change reference counts, and do not acquire the GIL,
making them safe to call from signal handlers and custom memory allocator hooks.
Loading
Loading