Skip to content

Commit 7cc60de

Browse files
Carl Meyerfacebook-github-bot
authored andcommitted
backport 3.12 code watchers
Summary: Backport of python/cpython#99859 (plus some more recent changes.) Reviewed By: alexmalyshev Differential Revision: D47201113 fbshipit-source-id: 9390ca289c043bc56ebfdd2ade63305347190a99
1 parent 8f5e972 commit 7cc60de

File tree

7 files changed

+385
-0
lines changed

7 files changed

+385
-0
lines changed

Cinder/module/known-core-python-exported-symbols

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,7 @@ PyCMethod_New
382382
PyCMethod_Type
383383
_Py_c_neg
384384
PyCode_Addr2Line
385+
PyCode_AddWatcher
385386
_PyCodeCache_RefType
386387
PyCodec_BackslashReplaceErrors
387388
PyCodec_Decode
@@ -398,6 +399,7 @@ PyCodec_IncrementalEncoder
398399
_PyCodecInfo_GetIncrementalDecoder
399400
_PyCodecInfo_GetIncrementalEncoder
400401
PyCodec_KnownEncoding
402+
PyCode_ClearWatcher
401403
_PyCodec_Lookup
402404
PyCodec_LookupError
403405
_PyCodec_LookupTextEncoding

Include/cpython/code.h

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,46 @@ PyCode_NewEmpty(const char *filename, const char *funcname, int firstlineno);
136136
use PyFrame_GetLineNumber() instead. */
137137
PyAPI_FUNC(int) PyCode_Addr2Line(PyCodeObject *, int);
138138

139+
#define PY_FOREACH_CODE_EVENT(V) \
140+
V(CREATE) \
141+
V(DESTROY)
142+
143+
typedef enum {
144+
#define PY_DEF_EVENT(op) PY_CODE_EVENT_##op,
145+
PY_FOREACH_CODE_EVENT(PY_DEF_EVENT)
146+
#undef PY_DEF_EVENT
147+
} PyCodeEvent;
148+
149+
150+
/*
151+
* A callback that is invoked for different events in a code object's lifecycle.
152+
*
153+
* The callback is invoked with a borrowed reference to co, after it is
154+
* created and before it is destroyed.
155+
*
156+
* If the callback sets an exception, it must return -1. Otherwise
157+
* it should return 0.
158+
*/
159+
typedef int (*PyCode_WatchCallback)(
160+
PyCodeEvent event,
161+
PyCodeObject* co);
162+
163+
/*
164+
* Register a per-interpreter callback that will be invoked for code object
165+
* lifecycle events.
166+
*
167+
* Returns a handle that may be passed to PyCode_ClearWatcher on success,
168+
* or -1 and sets an error if no more handles are available.
169+
*/
170+
PyAPI_FUNC(int) PyCode_AddWatcher(PyCode_WatchCallback callback);
171+
172+
/*
173+
* Clear the watcher associated with the watcher_id handle.
174+
*
175+
* Returns 0 on success or -1 if no watcher exists for the provided id.
176+
*/
177+
PyAPI_FUNC(int) PyCode_ClearWatcher(int watcher_id);
178+
139179
/* for internal use only */
140180
struct _opaque {
141181
int computed_line;

Include/internal/pycore_code.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
extern "C" {
55
#endif
66

7+
#define CODE_MAX_WATCHERS 8
8+
79
typedef struct {
810
PyObject *ptr; /* Cached pointer (borrowed reference) */
911
uint64_t globals_ver; /* ma_version of global dict */

Include/internal/pycore_interp.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
#include "pycore_atomic.h" // _Py_atomic_address
99
#include "pycore_ast_state.h" // struct ast_state
10+
#include "pycore_code.h" // CODE_MAX_WATCHERS
1011
#include "pycore_gil.h" // struct _gil_runtime_state
1112
#include "pycore_gc.h" // struct _gc_runtime_state
1213
#include "pycore_warnings.h" // struct _warnings_runtime_state
@@ -297,6 +298,9 @@ struct _is {
297298
struct atexit_state atexit;
298299

299300
PyObject *audit_hooks;
301+
PyCode_WatchCallback code_watchers[CODE_MAX_WATCHERS];
302+
// One bit is set for each non-NULL entry in code_watchers
303+
uint8_t active_code_watchers;
300304

301305
/* Small integers are preallocated in this array so that they
302306
can be shared.

Lib/test/test_capi.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1221,6 +1221,93 @@ def test_clear_unassigned_watcher_id(self):
12211221
self.clear_watcher(1)
12221222

12231223

1224+
class TestCodeObjectWatchers(unittest.TestCase):
1225+
@contextmanager
1226+
def code_watcher(self, which_watcher):
1227+
wid = _testcapi.add_code_watcher(which_watcher)
1228+
try:
1229+
yield wid
1230+
finally:
1231+
_testcapi.clear_code_watcher(wid)
1232+
1233+
def assert_event_counts(self, exp_created_0, exp_destroyed_0,
1234+
exp_created_1, exp_destroyed_1):
1235+
self.assertEqual(
1236+
exp_created_0, _testcapi.get_code_watcher_num_created_events(0))
1237+
self.assertEqual(
1238+
exp_destroyed_0, _testcapi.get_code_watcher_num_destroyed_events(0))
1239+
self.assertEqual(
1240+
exp_created_1, _testcapi.get_code_watcher_num_created_events(1))
1241+
self.assertEqual(
1242+
exp_destroyed_1, _testcapi.get_code_watcher_num_destroyed_events(1))
1243+
1244+
def test_code_object_events_dispatched(self):
1245+
# verify that all counts are zero before any watchers are registered
1246+
self.assert_event_counts(0, 0, 0, 0)
1247+
1248+
# verify that all counts remain zero when a code object is
1249+
# created and destroyed with no watchers registered
1250+
co1 = _testcapi.code_newempty("test_watchers", "dummy1", 0)
1251+
self.assert_event_counts(0, 0, 0, 0)
1252+
del co1
1253+
self.assert_event_counts(0, 0, 0, 0)
1254+
1255+
# verify counts are as expected when first watcher is registered
1256+
with self.code_watcher(0):
1257+
self.assert_event_counts(0, 0, 0, 0)
1258+
co2 = _testcapi.code_newempty("test_watchers", "dummy2", 0)
1259+
self.assert_event_counts(1, 0, 0, 0)
1260+
del co2
1261+
self.assert_event_counts(1, 1, 0, 0)
1262+
1263+
# again with second watcher registered
1264+
with self.code_watcher(1):
1265+
self.assert_event_counts(1, 1, 0, 0)
1266+
co3 = _testcapi.code_newempty("test_watchers", "dummy3", 0)
1267+
self.assert_event_counts(2, 1, 1, 0)
1268+
del co3
1269+
self.assert_event_counts(2, 2, 1, 1)
1270+
1271+
# verify counts are reset and don't change after both watchers are cleared
1272+
co4 = _testcapi.code_newempty("test_watchers", "dummy4", 0)
1273+
self.assert_event_counts(0, 0, 0, 0)
1274+
del co4
1275+
self.assert_event_counts(0, 0, 0, 0)
1276+
1277+
def test_error(self):
1278+
with self.code_watcher(2):
1279+
with catch_unraisable_exception() as cm:
1280+
co = _testcapi.code_newempty("test_watchers", "dummy0", 0)
1281+
1282+
self.assertEqual(
1283+
cm.unraisable.object,
1284+
f"PY_CODE_EVENT_CREATE watcher callback for {co!r}"
1285+
)
1286+
self.assertEqual(str(cm.unraisable.exc_value), "boom!")
1287+
1288+
def test_dealloc_error(self):
1289+
co = _testcapi.code_newempty("test_watchers", "dummy0", 0)
1290+
with self.code_watcher(2):
1291+
with catch_unraisable_exception() as cm:
1292+
del co
1293+
1294+
self.assertEqual(str(cm.unraisable.exc_value), "boom!")
1295+
1296+
def test_clear_out_of_range_watcher_id(self):
1297+
with self.assertRaisesRegex(ValueError, r"Invalid code watcher ID -1"):
1298+
_testcapi.clear_code_watcher(-1)
1299+
with self.assertRaisesRegex(ValueError, r"Invalid code watcher ID 8"):
1300+
_testcapi.clear_code_watcher(8) # CODE_MAX_WATCHERS = 8
1301+
1302+
def test_clear_unassigned_watcher_id(self):
1303+
with self.assertRaisesRegex(ValueError, r"No code watcher set for ID 1"):
1304+
_testcapi.clear_code_watcher(1)
1305+
1306+
def test_allocate_too_many_watchers(self):
1307+
with self.assertRaisesRegex(RuntimeError, r"no more code watcher IDs available"):
1308+
_testcapi.allocate_too_many_code_watchers()
1309+
1310+
12241311
class TestFuncWatchers(unittest.TestCase):
12251312
@contextmanager
12261313
def add_watcher(self, func):

Modules/_testcapimodule.c

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
#include "Python.h"
2121
#include "datetime.h"
2222
#include "marshal.h"
23+
#include "pycore_code.h"
2324
#include "structmember.h" // PyMemberDef
2425
#include <float.h>
2526
#include <signal.h>
@@ -6252,6 +6253,145 @@ get_dict_watcher_events(PyObject *self, PyObject *Py_UNUSED(args))
62526253
return Py_NewRef(g_dict_watch_events);
62536254
}
62546255

6256+
// Test code object watching
6257+
6258+
#define NUM_CODE_WATCHERS 2
6259+
static int num_code_object_created_events[NUM_CODE_WATCHERS] = {0, 0};
6260+
static int num_code_object_destroyed_events[NUM_CODE_WATCHERS] = {0, 0};
6261+
6262+
static int
6263+
handle_code_object_event(int which_watcher, PyCodeEvent event, PyCodeObject *co) {
6264+
if (event == PY_CODE_EVENT_CREATE) {
6265+
num_code_object_created_events[which_watcher]++;
6266+
}
6267+
else if (event == PY_CODE_EVENT_DESTROY) {
6268+
num_code_object_destroyed_events[which_watcher]++;
6269+
}
6270+
else {
6271+
return -1;
6272+
}
6273+
return 0;
6274+
}
6275+
6276+
static int
6277+
first_code_object_callback(PyCodeEvent event, PyCodeObject *co)
6278+
{
6279+
return handle_code_object_event(0, event, co);
6280+
}
6281+
6282+
static int
6283+
second_code_object_callback(PyCodeEvent event, PyCodeObject *co)
6284+
{
6285+
return handle_code_object_event(1, event, co);
6286+
}
6287+
6288+
static int
6289+
noop_code_event_handler(PyCodeEvent event, PyCodeObject *co)
6290+
{
6291+
return 0;
6292+
}
6293+
6294+
static int
6295+
error_code_event_handler(PyCodeEvent event, PyCodeObject *co)
6296+
{
6297+
PyErr_SetString(PyExc_RuntimeError, "boom!");
6298+
return -1;
6299+
}
6300+
6301+
static PyObject *
6302+
add_code_watcher(PyObject *self, PyObject *which_watcher)
6303+
{
6304+
int watcher_id;
6305+
assert(PyLong_Check(which_watcher));
6306+
long which_l = PyLong_AsLong(which_watcher);
6307+
if (which_l == 0) {
6308+
watcher_id = PyCode_AddWatcher(first_code_object_callback);
6309+
num_code_object_created_events[0] = 0;
6310+
num_code_object_destroyed_events[0] = 0;
6311+
}
6312+
else if (which_l == 1) {
6313+
watcher_id = PyCode_AddWatcher(second_code_object_callback);
6314+
num_code_object_created_events[1] = 0;
6315+
num_code_object_destroyed_events[1] = 0;
6316+
}
6317+
else if (which_l == 2) {
6318+
watcher_id = PyCode_AddWatcher(error_code_event_handler);
6319+
}
6320+
else {
6321+
PyErr_Format(PyExc_ValueError, "invalid watcher %d", which_l);
6322+
return NULL;
6323+
}
6324+
if (watcher_id < 0) {
6325+
return NULL;
6326+
}
6327+
return PyLong_FromLong(watcher_id);
6328+
}
6329+
6330+
static PyObject *
6331+
clear_code_watcher(PyObject *self, PyObject *watcher_id)
6332+
{
6333+
assert(PyLong_Check(watcher_id));
6334+
long watcher_id_l = PyLong_AsLong(watcher_id);
6335+
if (PyCode_ClearWatcher(watcher_id_l) < 0) {
6336+
return NULL;
6337+
}
6338+
// reset static events counters
6339+
if (watcher_id_l >= 0 && watcher_id_l < NUM_CODE_WATCHERS) {
6340+
num_code_object_created_events[watcher_id_l] = 0;
6341+
num_code_object_destroyed_events[watcher_id_l] = 0;
6342+
}
6343+
Py_RETURN_NONE;
6344+
}
6345+
6346+
static PyObject *
6347+
get_code_watcher_num_created_events(PyObject *self, PyObject *watcher_id)
6348+
{
6349+
assert(PyLong_Check(watcher_id));
6350+
long watcher_id_l = PyLong_AsLong(watcher_id);
6351+
assert(watcher_id_l >= 0 && watcher_id_l < NUM_CODE_WATCHERS);
6352+
return PyLong_FromLong(num_code_object_created_events[watcher_id_l]);
6353+
}
6354+
6355+
static PyObject *
6356+
get_code_watcher_num_destroyed_events(PyObject *self, PyObject *watcher_id)
6357+
{
6358+
assert(PyLong_Check(watcher_id));
6359+
long watcher_id_l = PyLong_AsLong(watcher_id);
6360+
assert(watcher_id_l >= 0 && watcher_id_l < NUM_CODE_WATCHERS);
6361+
return PyLong_FromLong(num_code_object_destroyed_events[watcher_id_l]);
6362+
}
6363+
6364+
static PyObject *
6365+
allocate_too_many_code_watchers(PyObject *self, PyObject *args)
6366+
{
6367+
int watcher_ids[CODE_MAX_WATCHERS + 1];
6368+
int num_watchers = 0;
6369+
for (unsigned long i = 0; i < sizeof(watcher_ids) / sizeof(int); i++) {
6370+
int watcher_id = PyCode_AddWatcher(noop_code_event_handler);
6371+
if (watcher_id == -1) {
6372+
break;
6373+
}
6374+
watcher_ids[i] = watcher_id;
6375+
num_watchers++;
6376+
}
6377+
PyObject *err_type, *err_val, *err_tb;
6378+
PyErr_Fetch(&err_type, &err_val, &err_tb);
6379+
for (int i = 0; i < num_watchers; i++) {
6380+
if (PyCode_ClearWatcher(watcher_ids[i]) < 0) {
6381+
PyErr_WriteUnraisable(Py_None);
6382+
break;
6383+
}
6384+
}
6385+
if (err_val) {
6386+
PyErr_Restore(err_type, err_val, err_tb);
6387+
return NULL;
6388+
}
6389+
else if (PyErr_Occurred()) {
6390+
return NULL;
6391+
}
6392+
Py_RETURN_NONE;
6393+
}
6394+
62556395
// Test function watchers
62566396

62576397
#define NUM_TEST_FUNC_WATCHERS 2
@@ -6764,6 +6904,14 @@ static PyMethodDef TestMethods[] = {
67646904
{"watch_dict", watch_dict, METH_VARARGS},
67656905
{"unwatch_dict", unwatch_dict, METH_VARARGS},
67666906
{"get_dict_watcher_events", get_dict_watcher_events, METH_NOARGS},
6907+
{"add_code_watcher", add_code_watcher, METH_O},
6908+
{"clear_code_watcher", clear_code_watcher, METH_O},
6909+
{"get_code_watcher_num_created_events",
6910+
get_code_watcher_num_created_events, METH_O},
6911+
{"get_code_watcher_num_destroyed_events",
6912+
get_code_watcher_num_destroyed_events, METH_O},
6913+
{"allocate_too_many_code_watchers",
6914+
(PyCFunction) allocate_too_many_code_watchers, METH_NOARGS},
67676915
{"add_func_watcher", add_func_watcher, METH_O},
67686916
{"clear_func_watcher", clear_func_watcher, METH_O},
67696917
{"set_func_defaults_via_capi", set_func_defaults_via_capi, METH_VARARGS},

0 commit comments

Comments
 (0)