Skip to content

Commit fb862ba

Browse files
committed
Add an optional callback that is invoked whenever a function is modified
JIT compilers may need to invalidate compiled code when a function is modified (e.g. if its code object is modified). This adds the ability to set a callback that, when set, is called each time a function is modified.
1 parent 553d3c1 commit fb862ba

File tree

14 files changed

+438
-3
lines changed

14 files changed

+438
-3
lines changed

Doc/c-api/function.rst

+53
Original file line numberDiff line numberDiff line change
@@ -118,3 +118,56 @@ There are a few functions specific to Python functions.
118118
must be a dictionary or ``Py_None``.
119119
120120
Raises :exc:`SystemError` and returns ``-1`` on failure.
121+
122+
123+
.. c:function:: int PyFunction_AddWatcher(PyFunction_WatchCallback callback)
124+
125+
Register *callback* as a function watcher for the current interpreter. Returns
126+
an id which may be passed to :c:func:`PyFunction_ClearWatcher`. In case
127+
of error (e.g. no more watcher IDs available), return ``-1`` and set an
128+
exception.
129+
130+
.. versionadded:: 3.12
131+
132+
133+
.. c:function:: int PyFunction_ClearWatcher(int watcher_id)
134+
135+
Clear watcher identified by *watcher_id* previously returned from
136+
:c:func:`PyFunction_AddWatcher` for the current interpreter. Return ``0`` on
137+
success or ``-1`` on error (e.g. if the given *watcher_id* was never
138+
registered.)
139+
140+
.. versionadded:: 3.12
141+
142+
143+
.. c:type:: PyFunction_WatchEvent
144+
145+
Enumeration of possible function watcher events: ``PyFunction_EVENT_CREATED``,
146+
``PyFunction_EVENT_DESTROY``, ``PyFunction_EVENT_MODIFY_CODE``,
147+
``PyFunction_EVENT_MODIFY_DEFAULTS``, or ``PyFunction_EVENT_MODIFY_KWDEFAULTS``.
148+
149+
.. versionadded:: 3.12
150+
151+
152+
.. c:type:: int (*PyFunction_WatchCallback)(PyFunction_WatchEvent event, PyFunctionObject *func, PyObject *new_value)
153+
154+
Type of a function watcher callback function.
155+
156+
If *event* is ``PyFunction_EVENT_CREATED`` or ``PyFunction_EVENT_DESTROY``
157+
then *new_value* will be ``NULL``. Otherwise, *new_value* will hold a
158+
borrowed reference to the new value that is about to be stored in *func* for
159+
the attribute that is being modified.
160+
161+
The callback may inspect but must not modify *func*; doing so could have
162+
unpredictable effects, including infinite recursion.
163+
164+
If *event* is ``PyFunction_EVENT_CREATED``, then the callback is invoked
165+
after `func` has been fully initialized. Otherwise, the callback is invoked
166+
before the modification to *func* takes place, so the prior state of *func*
167+
can be inspected.
168+
169+
If the callback returns with an exception set, it must return ``-1``; this
170+
exception will be printed as an unraisable exception using
171+
:c:func:`PyErr_WriteUnraisable`. Otherwise it should return ``0``.
172+
173+
.. versionadded:: 3.12

Include/cpython/funcobject.h

+46
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,52 @@ PyAPI_DATA(PyTypeObject) PyStaticMethod_Type;
131131
PyAPI_FUNC(PyObject *) PyClassMethod_New(PyObject *);
132132
PyAPI_FUNC(PyObject *) PyStaticMethod_New(PyObject *);
133133

134+
#define FOREACH_FUNC_EVENT(V) \
135+
V(CREATED) \
136+
V(DESTROY) \
137+
V(MODIFY_CODE) \
138+
V(MODIFY_DEFAULTS) \
139+
V(MODIFY_KWDEFAULTS)
140+
141+
typedef enum {
142+
#define DEF_EVENT(EVENT) PyFunction_EVENT_##EVENT,
143+
FOREACH_FUNC_EVENT(DEF_EVENT)
144+
#undef DEF_EVENT
145+
} PyFunction_WatchEvent;
146+
147+
/*
148+
* A callback that is invoked for different events in a function's lifecycle.
149+
*
150+
* The callback is invoked with a borrowed reference to func, after it is
151+
* created and before it is modified or destroyed. The callback should not
152+
* modify func.
153+
*
154+
* When a function's code object, defaults, or kwdefaults are modified the
155+
* callback will be invoked with the respective event and new_value will
156+
* contain a borrowed reference to the new value that is about to be stored in
157+
* the function. Otherwise the third argument is NULL.
158+
*/
159+
typedef int (*PyFunction_WatchCallback)(
160+
PyFunction_WatchEvent event,
161+
PyFunctionObject *func,
162+
PyObject *new_value);
163+
164+
/*
165+
* Register a per-interpreter callback that will be invoked for function lifecycle
166+
* events.
167+
*
168+
* Returns a handle that may be passed to PyFunction_ClearWatcher on success,
169+
* or -1 and sets an error if no more handles are available.
170+
*/
171+
PyAPI_FUNC(int) PyFunction_AddWatcher(PyFunction_WatchCallback callback);
172+
173+
/*
174+
* Clear the watcher associated with the watcher_id handle.
175+
*
176+
* Returns 0 on success or -1 if no watcher exists for the supplied id.
177+
*/
178+
PyAPI_FUNC(int) PyFunction_ClearWatcher(int watcher_id);
179+
134180
#ifdef __cplusplus
135181
}
136182
#endif

Include/internal/pycore_function.h

+2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ extern "C" {
88
# error "this header requires Py_BUILD_CORE define"
99
#endif
1010

11+
#define FUNC_MAX_WATCHERS 8
12+
1113
extern PyFunctionObject* _PyFunction_FromConstructor(PyFrameConstructor *constr);
1214

1315
extern uint32_t _PyFunction_GetVersionForCurrentState(PyFunctionObject *func);

Include/internal/pycore_interp.h

+2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ extern "C" {
1717
#include "pycore_dict.h" // struct _Py_dict_state
1818
#include "pycore_exceptions.h" // struct _Py_exc_state
1919
#include "pycore_floatobject.h" // struct _Py_float_state
20+
#include "pycore_function.h" // FUNC_MAX_WATCHERS
2021
#include "pycore_genobject.h" // struct _Py_async_gen_state
2122
#include "pycore_gc.h" // struct _gc_runtime_state
2223
#include "pycore_list.h" // struct _Py_list_state
@@ -147,6 +148,7 @@ struct _is {
147148
_PyFrameEvalFunction eval_frame;
148149

149150
PyDict_WatchCallback dict_watchers[DICT_MAX_WATCHERS];
151+
PyFunction_WatchCallback func_watchers[FUNC_MAX_WATCHERS];
150152

151153
Py_ssize_t co_extra_user_count;
152154
freefunc co_extra_freefuncs[MAX_CO_EXTRA_USERS];

Lib/test/test_func_events.py

+97
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import unittest
2+
3+
from contextlib import contextmanager
4+
from test.support import catch_unraisable_exception, import_helper
5+
6+
_testcapi = import_helper.import_module("_testcapi")
7+
8+
from _testcapi import (
9+
PYFUNC_EVENT_CREATED,
10+
PYFUNC_EVENT_DESTROY,
11+
PYFUNC_EVENT_MODIFY_CODE,
12+
PYFUNC_EVENT_MODIFY_DEFAULTS,
13+
PYFUNC_EVENT_MODIFY_KWDEFAULTS,
14+
_add_func_watcher,
15+
_clear_func_watcher,
16+
)
17+
18+
19+
class FuncEventsTest(unittest.TestCase):
20+
@contextmanager
21+
def add_watcher(self, func):
22+
wid = _add_func_watcher(func)
23+
try:
24+
yield
25+
finally:
26+
_clear_func_watcher(wid)
27+
28+
def test_func_events_dispatched(self):
29+
events = []
30+
def watcher(*args):
31+
events.append(args)
32+
33+
with self.add_watcher(watcher):
34+
def myfunc():
35+
pass
36+
self.assertIn((PYFUNC_EVENT_CREATED, myfunc, None), events)
37+
myfunc_id = id(myfunc)
38+
39+
new_code = self.test_func_events_dispatched.__code__
40+
myfunc.__code__ = new_code
41+
self.assertIn((PYFUNC_EVENT_MODIFY_CODE, myfunc, new_code), events)
42+
43+
new_defaults = (123,)
44+
myfunc.__defaults__ = new_defaults
45+
self.assertIn((PYFUNC_EVENT_MODIFY_DEFAULTS, myfunc, new_defaults), events)
46+
47+
new_kwdefaults = {"self": 123}
48+
myfunc.__kwdefaults__ = new_kwdefaults
49+
self.assertIn((PYFUNC_EVENT_MODIFY_KWDEFAULTS, myfunc, new_kwdefaults), events)
50+
51+
# Clear events reference to func
52+
events = []
53+
del myfunc
54+
self.assertIn((PYFUNC_EVENT_DESTROY, myfunc_id, None), events)
55+
56+
def test_multiple_watchers(self):
57+
events0 = []
58+
def first_watcher(*args):
59+
events0.append(args)
60+
61+
events1 = []
62+
def second_watcher(*args):
63+
events1.append(args)
64+
65+
with self.add_watcher(first_watcher):
66+
with self.add_watcher(second_watcher):
67+
def myfunc():
68+
pass
69+
70+
event = (PYFUNC_EVENT_CREATED, myfunc, None)
71+
self.assertIn(event, events0)
72+
self.assertIn(event, events1)
73+
74+
def test_watcher_raises_error(self):
75+
class MyError(Exception):
76+
pass
77+
78+
def watcher(*args):
79+
raise MyError("testing 123")
80+
81+
with self.add_watcher(watcher):
82+
with catch_unraisable_exception() as cm:
83+
def myfunc():
84+
pass
85+
86+
self.assertIs(cm.unraisable.object, myfunc)
87+
self.assertIsInstance(cm.unraisable.exc_value, MyError)
88+
89+
def test_clear_out_of_range_watcher_id(self):
90+
with self.assertRaisesRegex(ValueError, r"Invalid func watcher ID -1"):
91+
_clear_func_watcher(-1)
92+
with self.assertRaisesRegex(ValueError, r"Invalid func watcher ID 8"):
93+
_clear_func_watcher(8) # FUNC_MAX_WATCHERS = 8
94+
95+
def test_clear_unassigned_watcher_id(self):
96+
with self.assertRaisesRegex(ValueError, r"No func watcher set for ID 1"):
97+
_clear_func_watcher(1)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Optimizing interpreters and JIT compilers may need to invalidate internal
2+
metadata when functions are modified. This change adds the ability to
3+
provide a callback that will be invoked each time a function is created,
4+
modified, or destroyed.

Modules/Setup.stdlib.in

+1-1
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@
169169
@MODULE__XXTESTFUZZ_TRUE@_xxtestfuzz _xxtestfuzz/_xxtestfuzz.c _xxtestfuzz/fuzzer.c
170170
@MODULE__TESTBUFFER_TRUE@_testbuffer _testbuffer.c
171171
@MODULE__TESTINTERNALCAPI_TRUE@_testinternalcapi _testinternalcapi.c
172-
@MODULE__TESTCAPI_TRUE@_testcapi _testcapimodule.c _testcapi/vectorcall.c _testcapi/vectorcall_limited.c _testcapi/heaptype.c _testcapi/unicode.c
172+
@MODULE__TESTCAPI_TRUE@_testcapi _testcapimodule.c _testcapi/vectorcall.c _testcapi/vectorcall_limited.c _testcapi/heaptype.c _testcapi/unicode.c _testcapi/func_events.c
173173

174174
# Some testing modules MUST be built as shared libraries.
175175
*shared*

0 commit comments

Comments
 (0)