Skip to content
Draft
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
51 changes: 51 additions & 0 deletions Doc/c-api/frame.rst
Original file line number Diff line number Diff line change
Expand Up @@ -243,3 +243,54 @@ 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


External frame introspectors
^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Frames pushed by something other than the VM carry an external frame
introspector, which tells the :term:`free-threaded <free threading>` garbage
collector how to visit references held in external frame state.

.. c:var:: PyTypeObject PyUnstable_ExternalFrameIntrospector_Type

The type of external frame introspector objects.

Instances cannot be created from Python code. Use
:c:func:`PyUnstable_ExternalFrameIntrospector_New` to create one.

.. versionadded:: 3.16


.. c:function:: int PyUnstable_ExternalFrameIntrospector_Check(PyObject *obj)

Return non-zero if *obj* is an external frame introspector object.

.. versionadded:: 3.16


.. c:type:: int (*PyUnstable_ExternalFrameGCVisitor)(PyInterpreterState *interp, struct _PyInterpreterFrame *frame, gcvisitobjects_t callback, void *arg)

The garbage-collector visitor held by an external frame introspector.

The collector calls it with the interpreter being collected, the
external frame, and a *callback* to invoke once for each :c:type:`PyObject`
referenced by external frame state (passing *arg* through). The callback
follows :c:type:`gcvisitobjects_t` conventions.

Return 0 if the frame is complete (all references were visited), or
non-zero if the frame is incomplete and the collector must skip collecting
objects with deferred references.

.. versionadded:: 3.16


.. c:function:: PyObject* PyUnstable_ExternalFrameIntrospector_New(PyUnstable_ExternalFrameGCVisitor gc_visitor)

Create a new external frame introspector holding *gc_visitor*.

Return a :term:`strong reference` to the new introspector on success.
Raise :exc:`ValueError` and return ``NULL`` if *gc_visitor* is ``NULL``;
raise :exc:`MemoryError` and return ``NULL`` on allocation failure.

.. versionadded:: 3.16
49 changes: 49 additions & 0 deletions Include/cpython/pyframe.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,52 @@ PyAPI_FUNC(int) PyUnstable_InterpreterFrame_GetLasti(struct _PyInterpreterFrame
/* Returns the currently executing line number, or -1 if there is no line number.
* Does not raise an exception. */
PyAPI_FUNC(int) PyUnstable_InterpreterFrame_GetLine(struct _PyInterpreterFrame *frame);

/* External frame introspection.
*
* Frames with the `external` bit set were pushed by something other than
* the VM. Such frames have a `struct _PyInterpreterFrameExternal` header
* immediately before the `struct _PyInterpreterFrame` in memory. The header
* holds a strong reference to a `_PyExternalFrameIntrospector` object which
* knows how to visit external references for the garbage collector.
*
* The introspector is created with a GC visitor function. During collection
* (free-threaded build), the GC calls the visitor with the frame and a
* callback; the visitor must invoke the callback once for each PyObject*
* referenced by external frame state.
*/

struct _PyExternalFrameIntrospector;

/* GC visitor for external frames.
*
* `interp` is the interpreter being collected, `frame` is the external
* frame, `callback` must be invoked once for each PyObject* referenced
* by external frame state (with `arg` passed through). The callback
* follows gcvisitobjects_t conventions (return 1 to continue, 0 to stop).
*
* Returns 0 if the frame is complete (all references were visited),
* nonzero if the frame is incomplete and the GC must skip collecting
* objects with deferred references.
*/
typedef int (*PyUnstable_ExternalFrameGCVisitor)(
PyInterpreterState *interp,
struct _PyInterpreterFrame *frame,
gcvisitobjects_t callback,
void *arg);

PyAPI_DATA(PyTypeObject) PyUnstable_ExternalFrameIntrospector_Type;

#define PyUnstable_ExternalFrameIntrospector_Check(op) \
Py_IS_TYPE((op), &PyUnstable_ExternalFrameIntrospector_Type)

/* Create a new _PyExternalFrameIntrospector holding `gc_visitor`.
* Returns a new reference, or NULL with an exception set if `gc_visitor`
* is NULL or allocation fails. */
PyAPI_FUNC(PyObject *) PyUnstable_ExternalFrameIntrospector_New(
PyUnstable_ExternalFrameGCVisitor gc_visitor);

/* Return the gc_visitor stored in `introspector`.
* Returns NULL with a TypeError if `introspector` is not an introspector. */
PyAPI_FUNC(PyUnstable_ExternalFrameGCVisitor)
PyUnstable_ExternalFrameIntrospector_GetGCVisitor(PyObject *introspector);
26 changes: 26 additions & 0 deletions Include/internal/pycore_interpframe.h
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,8 @@ static inline void _PyFrame_Copy(_PyInterpreterFrame *src, _PyInterpreterFrame *
dest->stackpointer = dest->localsplus + stacktop;
// visited is GC bookkeeping for the current stack walk, not frame state.
dest->visited = 0;
// Copies never carry the external header, so they are never external.
dest->external = 0;
#ifdef Py_DEBUG
dest->stackpointer_valid = src->stackpointer_valid;
dest->lltrace = src->lltrace;
Expand Down Expand Up @@ -208,6 +210,7 @@ _PyFrame_Initialize(
frame->return_offset = 0;
frame->owner = FRAME_OWNED_BY_THREAD;
frame->visited = 0;
frame->external = 0;
#ifdef Py_DEBUG
frame->stackpointer_valid = 1;
frame->lltrace = 0;
Expand Down Expand Up @@ -315,6 +318,28 @@ _PyThreadState_GetFrame(PyThreadState *tstate)
return _PyFrame_GetFirstComplete(tstate->current_frame);
}

/* Convert between a frame with the `external` bit set and its header
* which lives immediately before the frame in memory. */
static inline _PyInterpreterFrameExternal *
_PyInterpreterFrame_GetExternal(_PyInterpreterFrame *frame)
{
assert(frame->external);
return (_PyInterpreterFrameExternal *)((char *)frame -
sizeof(_PyInterpreterFrameExternal));
}

static inline _PyInterpreterFrame *
_PyInterpreterFrameExternal_GetFrame(_PyInterpreterFrameExternal *external)
{
return (_PyInterpreterFrame *)(external + 1);
}

static inline PyObject *
_PyInterpreterFrameExternal_GetIntrospector(_PyInterpreterFrameExternal *external)
{
return PyStackRef_AsPyObjectBorrow(external->introspector);
}

// Update last_profiled_frame for remote profiler frame caching.
// Only update if we're removing the exact frame that was last profiled.
// This avoids corrupting the cache when transient frames (called and returned
Expand Down Expand Up @@ -441,6 +466,7 @@ _PyFrame_PushTrampolineUnchecked(PyThreadState *tstate, PyCodeObject *code, int
#endif
frame->owner = FRAME_OWNED_BY_THREAD;
frame->visited = 0;
frame->external = 0;
#ifdef Py_DEBUG
frame->stackpointer_valid = 1;
frame->lltrace = 0;
Expand Down
21 changes: 21 additions & 0 deletions Include/internal/pycore_interpframe_structs.h
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,27 @@ struct _PyInterpreterFrame {
_PyStackRef localsplus[1];
};

/* Header for externally-pushed frames.
*
* When a frame's `external` bit is set, a `_PyInterpreterFrameExternal`
* header lives immediately before the `_PyInterpreterFrame` in memory.
* It holds a strong reference to a `_PyExternalFrameIntrospector` which
* knows how to visit external references for the garbage collector.
*
* Layout in memory:
*
* +-------------------------------+----------------------+
* | _PyInterpreterFrameExternal | _PyInterpreterFrame |
* +-------------------------------+----------------------+
* ^ ^
* external frame
*
* Use `_PyInterpreterFrame_GetExternal()` to convert from frame to header.
*/
struct _PyInterpreterFrameExternal {
_PyStackRef introspector; /* Strong reference to _PyExternalFrameIntrospector */
};


/* _PyGenObject_HEAD defines the initial segment of generator
and coroutine objects. */
Expand Down
1 change: 1 addition & 0 deletions Include/internal/pycore_typedefs.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ extern "C" {
#endif

typedef struct _PyInterpreterFrame _PyInterpreterFrame;
typedef struct _PyInterpreterFrameExternal _PyInterpreterFrameExternal;
typedef struct pyruntimestate _PyRuntimeState;

#ifdef __cplusplus
Expand Down
59 changes: 59 additions & 0 deletions Lib/test/test_capi/test_frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,64 @@ def dummy():
self.assertIsNone(frame.f_back)


class ExternalFrameTest(unittest.TestCase):
def test_introspector_new(self):
intro = _testcapi.external_frame_introspector_new()
self.assertEqual(type(intro).__name__, "external_frame_introspector")
self.assertTrue(_testcapi.external_frame_introspector_check(intro))
self.assertFalse(_testcapi.external_frame_introspector_check(None))
self.assertFalse(_testcapi.external_frame_introspector_check(42))
self.assertFalse(_testcapi.external_frame_introspector_check([]))

def test_introspector_new_null(self):
with self.assertRaises(ValueError):
_testcapi.external_frame_introspector_new_null()

def test_introspector_roundtrip(self):
self.assertTrue(_testcapi.external_frame_introspector_roundtrip())

def test_introspector_get_visitor(self):
intro = _testcapi.external_frame_introspector_new()
addr = _testcapi.external_frame_introspector_get_visitor(intro)
self.assertIsInstance(addr, int)
self.assertNotEqual(addr, 0)
with self.assertRaises(TypeError):
_testcapi.external_frame_introspector_get_visitor(None)
with self.assertRaises(TypeError):
_testcapi.external_frame_introspector_get_visitor(42)

def test_gc_visit_external_frame(self):
_testinternalcapi = import_helper.import_module('_testinternalcapi')
if sys._is_gil_enabled():
self.skipTest("requires free-threaded build")
obj = [1, 2, 3]
calls, cb_calls = _testinternalcapi.external_frame_gc_test(obj, 0)
self.assertGreaterEqual(calls, 1)
self.assertGreaterEqual(cb_calls, 1)

def test_gc_visit_external_frame_incomplete(self):
_testinternalcapi = import_helper.import_module('_testinternalcapi')
if sys._is_gil_enabled():
self.skipTest("requires free-threaded build")
obj = [1, 2, 3]
calls, cb_calls = _testinternalcapi.external_frame_gc_test(obj, 1)
self.assertGreaterEqual(calls, 1)
self.assertGreaterEqual(cb_calls, 1)

def test_frame_init_clears_external_bits(self):
# VM-created frames must never carry the external bit, otherwise
# the GC would read a non-existent external header (segfault).
_testinternalcapi = import_helper.import_module('_testinternalcapi')
def func():
pass
self.assertIsNone(
_testinternalcapi.frame_init_clears_external_bits(func))

def test_no_external_frames_on_stack(self):
_testinternalcapi = import_helper.import_module('_testinternalcapi')
self.assertIsNone(
_testinternalcapi.frame_no_external_frames_on_stack())


if __name__ == "__main__":
unittest.main()
80 changes: 80 additions & 0 deletions Modules/_testcapi/frame.c
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,76 @@ frame_getvarstring(PyObject *self, PyObject *args)
}


static int
dummy_external_gc_visitor(PyInterpreterState *interp,
struct _PyInterpreterFrame *frame,
gcvisitobjects_t callback, void *arg)
{
(void)interp;
(void)frame;
(void)callback;
(void)arg;
return 0;
}

static PyObject *
external_frame_introspector_new(PyObject *self, PyObject *Py_UNUSED(ignored))
{
return PyUnstable_ExternalFrameIntrospector_New(dummy_external_gc_visitor);
}

static PyObject *
external_frame_introspector_new_null(PyObject *self, PyObject *Py_UNUSED(ignored))
{
return PyUnstable_ExternalFrameIntrospector_New(NULL);
}

static PyObject *
external_frame_introspector_check(PyObject *self, PyObject *obj)
{
return PyBool_FromLong(PyUnstable_ExternalFrameIntrospector_Check(obj));
}

static PyObject *
external_frame_introspector_roundtrip(PyObject *self, PyObject *Py_UNUSED(ignored))
{
PyObject *intro = PyUnstable_ExternalFrameIntrospector_New(
dummy_external_gc_visitor);
if (intro == NULL) {
return NULL;
}
if (!PyUnstable_ExternalFrameIntrospector_Check(intro)) {
Py_DECREF(intro);
PyErr_SetString(PyExc_AssertionError, "Check() failed");
return NULL;
}
PyUnstable_ExternalFrameGCVisitor visitor =
PyUnstable_ExternalFrameIntrospector_GetGCVisitor(intro);
if (visitor == NULL) {
Py_DECREF(intro);
return NULL;
}
if (visitor != dummy_external_gc_visitor) {
Py_DECREF(intro);
PyErr_SetString(PyExc_AssertionError, "visitor mismatch");
return NULL;
}
Py_DECREF(intro);
Py_RETURN_TRUE;
}

static PyObject *
external_frame_introspector_get_visitor(PyObject *self, PyObject *obj)
{
PyUnstable_ExternalFrameGCVisitor visitor =
PyUnstable_ExternalFrameIntrospector_GetGCVisitor(obj);
if (visitor == NULL) {
return NULL;
}
// Return the visitor address as an int for comparison in Python.
return PyLong_FromVoidPtr((void *)visitor);
}

static PyMethodDef test_methods[] = {
{"frame_getlocals", frame_getlocals, METH_O, NULL},
{"frame_getglobals", frame_getglobals, METH_O, NULL},
Expand All @@ -123,6 +193,16 @@ static PyMethodDef test_methods[] = {
{"frame_new", frame_new, METH_VARARGS, NULL},
{"frame_getvar", frame_getvar, METH_VARARGS, NULL},
{"frame_getvarstring", frame_getvarstring, METH_VARARGS, NULL},
{"external_frame_introspector_new", external_frame_introspector_new,
METH_NOARGS, NULL},
{"external_frame_introspector_new_null", external_frame_introspector_new_null,
METH_NOARGS, NULL},
{"external_frame_introspector_check", external_frame_introspector_check,
METH_O, NULL},
{"external_frame_introspector_roundtrip",
external_frame_introspector_roundtrip, METH_NOARGS, NULL},
{"external_frame_introspector_get_visitor",
external_frame_introspector_get_visitor, METH_O, NULL},
{NULL},
};

Expand Down
Loading
Loading