Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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
1 change: 1 addition & 0 deletions Include/cpython/funcobject.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ typedef struct {
PyObject *func_annotations; /* Annotations, a dict or NULL */
PyObject *func_annotate; /* Callable to fill the annotations dictionary */
PyObject *func_typeparams; /* Tuple of active type variables or NULL */
PyObject *func_old_codes; /* List of past code objects or NULL */
vectorcallfunc vectorcall;
/* Version number for use by specializer.
* Can set to non-zero when we want to specialize.
Expand Down
15 changes: 12 additions & 3 deletions Include/internal/pycore_interpframe.h
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ _PyFrame_NumSlotsForCodeObject(PyCodeObject *code)

static inline void _PyFrame_Copy(_PyInterpreterFrame *src, _PyInterpreterFrame *dest)
{
dest->f_executable = PyStackRef_MakeHeapSafe(src->f_executable);
dest->f_executable = PyStackRef_Borrow(src->f_executable);
// Don't leave a dangling pointer to the old frame when creating generators
// and coroutines:
dest->previous = NULL;
Expand Down Expand Up @@ -160,6 +160,15 @@ static inline void _PyFrame_Copy(_PyInterpreterFrame *src, _PyInterpreterFrame *
}
}

/* Generator frames need a strong reference to the code object */
static inline void
_PyFrame_CopyForGenerators(_PyInterpreterFrame *old_frame, _PyInterpreterFrame *gen_frame)
{
_PyFrame_Copy(old_frame, gen_frame);
gen_frame->owner = FRAME_OWNED_BY_GENERATOR;
gen_frame->f_executable = PyStackRef_MakeHeapSafe(gen_frame->f_executable);
}

#ifdef Py_GIL_DISABLED
static inline void
_PyFrame_InitializeTLBC(PyThreadState *tstate, _PyInterpreterFrame *frame,
Expand Down Expand Up @@ -191,7 +200,7 @@ _PyFrame_Initialize(
{
frame->previous = previous;
frame->f_funcobj = func;
frame->f_executable = PyStackRef_FromPyObjectNew(code);
frame->f_executable = PyStackRef_FromPyObjectBorrow((PyObject *)code);
PyFunctionObject *func_obj = (PyFunctionObject *)PyStackRef_AsPyObjectBorrow(func);
frame->f_builtins = func_obj->func_builtins;
frame->f_globals = func_obj->func_globals;
Expand Down Expand Up @@ -424,7 +433,7 @@ _PyFrame_PushTrampolineUnchecked(PyThreadState *tstate, PyCodeObject *code, int
assert(tstate->datastack_top < tstate->datastack_limit);
frame->previous = previous;
frame->f_funcobj = PyStackRef_None;
frame->f_executable = PyStackRef_FromPyObjectNew(code);
frame->f_executable = PyStackRef_FromPyObjectBorrow((PyObject *)code);
#ifdef Py_DEBUG
frame->f_builtins = NULL;
frame->f_globals = NULL;
Expand Down
4 changes: 3 additions & 1 deletion Include/internal/pycore_interpframe_structs.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ enum _frameowner {
};

struct _PyInterpreterFrame {
_PyStackRef f_executable; /* Deferred or strong reference (code object or None) */
/* Borrowed reference (code object or None) */
/* Strong reference for generators */

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be a borrowed reference for generators as well.
The function object still keeps the code object alive, regardless of whether it is a generator function or not.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if there is a trivial way for a generator to hold onto a borrowed reference since for a generator g, you can always access the code object by doing g.gi_code. The function can get freed but the code object is still needed. See, for example, this test:

def test_generator_resurrect(self):
# Test that a resurrected generator still has a valid gi_code
resurrected = []
# Resurrect a generator in a finalizer
exec(textwrap.dedent("""
def gen():
try:
yield
except:
resurrected.append(g)
g = gen()
next(g)
"""), {"resurrected": resurrected})
support.gc_collect()
self.assertEqual(len(resurrected), 1)
self.assertIsInstance(resurrected[0].gi_code, types.CodeType)

_PyStackRef f_executable;
Comment thread
brijkapadia marked this conversation as resolved.
Outdated
struct _PyInterpreterFrame *previous;
_PyStackRef f_funcobj; /* Deferred or strong reference. Only valid if not on C stack */
PyObject *f_globals; /* Borrowed reference. Only valid if not on C stack */
Expand Down
22 changes: 22 additions & 0 deletions Lib/test/test_capi/test_function.py
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,28 @@ def annofn(arg: int) -> str:
with self.assertRaises(SystemError):
_testcapi.function_get_annotations(None)

def test_function_old_codes(self):
def f():
pass

def g():
pass

def h():
pass

old_codes = _testcapi.function_get_old_codes(f)
Comment thread
brijkapadia marked this conversation as resolved.
Outdated
self.assertIsNone(old_codes)

f.__code__ = g.__code__
old_codes = _testcapi.function_get_old_codes(f)
self.assertIsInstance(old_codes, list)
self.assertEqual(len(old_codes), 1)

f.__code__ = h.__code__
old_codes = _testcapi.function_get_old_codes(f)
self.assertEqual(len(old_codes), 2)

# TODO: test PyFunction_New()
# TODO: test PyFunction_NewWithQualName()
# TODO: test PyFunction_SetVectorcall()
Expand Down
2 changes: 1 addition & 1 deletion Lib/test/test_sys.py
Original file line number Diff line number Diff line change
Expand Up @@ -1703,7 +1703,7 @@ def func():
check(x, size('3PiccPPP' + INTERPRETER_FRAME + 'P'))
# function
def func(): pass
check(func, size('16Pi'))
check(func, size('17Pi'))
class c():
@staticmethod
def foo():
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Avoid reference counting of :class:`code` objects when creating and destroying
frames by having functions retain a list of all past :class:`code` objects.

14 changes: 14 additions & 0 deletions Modules/_testcapi/function.c
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,19 @@ function_get_annotations(PyObject *self, PyObject *func)
}


static PyObject *
function_get_old_codes(PyObject *self, PyObject *func)
{
PyFunctionObject *func_o = (PyFunctionObject *) func;
PyObject *old_codes = func_o->func_old_codes;
if (old_codes == NULL) {
Py_RETURN_NONE;
}

return Py_NewRef(old_codes);
}


static PyMethodDef test_methods[] = {
{"function_get_code", function_get_code, METH_O, NULL},
{"function_get_globals", function_get_globals, METH_O, NULL},
Expand All @@ -141,6 +154,7 @@ static PyMethodDef test_methods[] = {
{"function_get_closure", function_get_closure, METH_O, NULL},
{"function_set_closure", function_set_closure, METH_VARARGS, NULL},
{"function_get_annotations", function_get_annotations, METH_O, NULL},
{"function_get_old_codes", function_get_old_codes, METH_O, NULL},
{NULL},
};

Expand Down
3 changes: 1 addition & 2 deletions Modules/_testinternalcapi/test_cases.c.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions Objects/funcobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ _PyFunction_FromConstructor(PyFrameConstructor *constr)
op->func_typeparams = NULL;
op->vectorcall = _PyFunction_Vectorcall;
op->func_version = FUNC_VERSION_UNSET;
op->func_old_codes = NULL;
// NOTE: functions created via FrameConstructor do not use deferred
// reference counting because they are typically not part of cycles
// nor accessed by multiple threads.
Expand Down Expand Up @@ -223,6 +224,7 @@ PyFunction_NewWithQualName(PyObject *code, PyObject *globals, PyObject *qualname
op->func_typeparams = NULL;
op->vectorcall = _PyFunction_Vectorcall;
op->func_version = FUNC_VERSION_UNSET;
op->func_old_codes = NULL;
if (((code_obj->co_flags & CO_NESTED) == 0) ||
(code_obj->co_flags & CO_METHOD)) {
// Use deferred reference counting for top-level functions, but not
Expand Down Expand Up @@ -686,6 +688,17 @@ func_set_code(PyObject *self, PyObject *value, void *Py_UNUSED(ignored))

handle_func_event(PyFunction_EVENT_MODIFY_CODE, op, value);
_PyFunction_ClearVersion(op);
if (op->func_old_codes == NULL) {
op->func_old_codes = PyList_New(0);
if (op->func_old_codes == NULL) {
return -1;
}
}

if (PyList_Append(op->func_old_codes, op->func_code) < 0) {
return -1;
}

Py_XSETREF(op->func_code, Py_NewRef(value));
return 0;
}
Expand Down Expand Up @@ -1114,6 +1127,7 @@ func_clear(PyObject *self)
Py_CLEAR(op->func_annotations);
Py_CLEAR(op->func_annotate);
Py_CLEAR(op->func_typeparams);
Py_CLEAR(op->func_old_codes);
// Don't Py_CLEAR(op->func_code), since code is always required
// to be non-NULL. Similarly, name and qualname shouldn't be NULL.
// However, name and qualname could be str subclasses, so they
Expand Down Expand Up @@ -1169,6 +1183,7 @@ func_traverse(PyObject *self, visitproc visit, void *arg)
Py_VISIT(f->func_annotate);
Py_VISIT(f->func_typeparams);
Py_VISIT(f->func_qualname);
Py_VISIT(f->func_old_codes);
return 0;
}

Expand Down
3 changes: 1 addition & 2 deletions Objects/genobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -1176,11 +1176,10 @@ gen_new_with_qualname(PyTypeObject *type, PyFrameObject *f,
assert(f->f_frame->frame_obj == NULL);
assert(f->f_frame->owner == FRAME_OWNED_BY_FRAME_OBJECT);
_PyInterpreterFrame *frame = &gen->gi_iframe;
_PyFrame_Copy((_PyInterpreterFrame *)f->_f_frame_data, frame);
_PyFrame_CopyForGenerators((_PyInterpreterFrame *)f->_f_frame_data, frame);
gen->gi_frame_state = FRAME_CREATED;
assert(frame->frame_obj == f);
f->f_frame = frame;
frame->owner = FRAME_OWNED_BY_GENERATOR;
assert(PyObject_GC_IsTracked((PyObject *)f));
Py_DECREF(f);
gen->gi_weakreflist = NULL;
Expand Down
3 changes: 1 addition & 2 deletions Python/bytecodes.c
Original file line number Diff line number Diff line change
Expand Up @@ -5854,10 +5854,9 @@ dummy_func(
SAVE_STACK();
_PyInterpreterFrame *gen_frame = &gen->gi_iframe;
frame->instr_ptr++;
_PyFrame_Copy(frame, gen_frame);
_PyFrame_CopyForGenerators(frame, gen_frame);
assert(frame->frame_obj == NULL);
gen->gi_frame_state = FRAME_CREATED;
gen_frame->owner = FRAME_OWNED_BY_GENERATOR;
_Py_LeaveRecursiveCallPy(tstate);
_PyInterpreterFrame *prev = frame->previous;
_PyThreadState_UpdateLastProfiledFrame(tstate, frame, prev);
Expand Down
3 changes: 1 addition & 2 deletions Python/executor_cases.c.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions Python/generated_cases.c.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading