Skip to content

Commit

Permalink
GH-91054: Add code object watchers API (GH-99859)
Browse files Browse the repository at this point in the history
* Add API to allow extensions to set callback function on creation and destruction of PyCodeObject

Co-authored-by: Ye11ow-Flash <[email protected]>
  • Loading branch information
itamaro and Ye11ow-Flash authored Dec 2, 2022
1 parent 0563be2 commit 3c137dc
Show file tree
Hide file tree
Showing 11 changed files with 364 additions and 0 deletions.
48 changes: 48 additions & 0 deletions Doc/c-api/code.rst
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,51 @@ bound into a function.
the free variables. On error, ``NULL`` is returned and an exception is raised.
.. versionadded:: 3.11
.. c:function:: int PyCode_AddWatcher(PyCode_WatchCallback callback)
Register *callback* as a code object watcher for the current interpreter.
Return an ID which may be passed to :c:func:`PyCode_ClearWatcher`.
In case of error (e.g. no more watcher IDs available),
return ``-1`` and set an exception.
.. versionadded:: 3.12
.. c:function:: int PyCode_ClearWatcher(int watcher_id)
Clear watcher identified by *watcher_id* previously returned from
:c:func:`PyCode_AddWatcher` for the current interpreter.
Return ``0`` on success, or ``-1`` and set an exception on error
(e.g. if the given *watcher_id* was never registered.)
.. versionadded:: 3.12
.. c:type:: PyCodeEvent
Enumeration of possible code object watcher events:
- ``PY_CODE_EVENT_CREATE``
- ``PY_CODE_EVENT_DESTROY``
.. versionadded:: 3.12
.. c:type:: int (*PyCode_WatchCallback)(PyCodeEvent event, PyCodeObject* co)
Type of a code object watcher callback function.
If *event* is ``PY_CODE_EVENT_CREATE``, then the callback is invoked
after `co` has been fully initialized. Otherwise, the callback is invoked
before the destruction of *co* takes place, so the prior state of *co*
can be inspected.
Users of this API should not rely on internal runtime implementation
details. Such details may include, but are not limited to, the exact
order and timing of creation and destruction of code objects. While
changes in these details may result in differences observable by watchers
(including whether a callback is invoked or not), it does not change
the semantics of the Python code being executed.
If the callback returns with an exception set, it must return ``-1``; this
exception will be printed as an unraisable exception using
:c:func:`PyErr_WriteUnraisable`. Otherwise it should return ``0``.
.. versionadded:: 3.12
4 changes: 4 additions & 0 deletions Doc/whatsnew/3.12.rst
Original file line number Diff line number Diff line change
Expand Up @@ -773,6 +773,10 @@ New Features
callbacks to receive notification on changes to a type.
(Contributed by Carl Meyer in :gh:`91051`.)

* Added :c:func:`PyCode_AddWatcher` and :c:func:`PyCode_ClearWatcher`
APIs to register callbacks to receive notification on creation and
destruction of code objects.
(Contributed by Itamar Ostricher in :gh:`91054`.)

* Add :c:func:`PyFrame_GetVar` and :c:func:`PyFrame_GetVarString` functions to
get a frame variable by its name.
Expand Down
35 changes: 35 additions & 0 deletions Include/cpython/code.h
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,41 @@ PyAPI_FUNC(int) PyCode_Addr2Line(PyCodeObject *, int);

PyAPI_FUNC(int) PyCode_Addr2Location(PyCodeObject *, int, int *, int *, int *, int *);

typedef enum PyCodeEvent {
PY_CODE_EVENT_CREATE,
PY_CODE_EVENT_DESTROY
} PyCodeEvent;


/*
* A callback that is invoked for different events in a code object's lifecycle.
*
* The callback is invoked with a borrowed reference to co, after it is
* created and before it is destroyed.
*
* If the callback returns with an exception set, it must return -1. Otherwise
* it should return 0.
*/
typedef int (*PyCode_WatchCallback)(
PyCodeEvent event,
PyCodeObject* co);

/*
* Register a per-interpreter callback that will be invoked for code object
* lifecycle events.
*
* Returns a handle that may be passed to PyCode_ClearWatcher on success,
* or -1 and sets an error if no more handles are available.
*/
PyAPI_FUNC(int) PyCode_AddWatcher(PyCode_WatchCallback callback);

/*
* Clear the watcher associated with the watcher_id handle.
*
* Returns 0 on success or -1 if no watcher exists for the provided id.
*/
PyAPI_FUNC(int) PyCode_ClearWatcher(int watcher_id);

/* for internal use only */
struct _opaque {
int computed_line;
Expand Down
2 changes: 2 additions & 0 deletions Include/internal/pycore_code.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
extern "C" {
#endif

#define CODE_MAX_WATCHERS 8

/* PEP 659
* Specialization and quickening structs and helper functions
*/
Expand Down
3 changes: 3 additions & 0 deletions Include/internal/pycore_interp.h
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,9 @@ struct _is {

PyObject *audit_hooks;
PyType_WatchCallback type_watchers[TYPE_MAX_WATCHERS];
PyCode_WatchCallback code_watchers[CODE_MAX_WATCHERS];
// One bit is set for each non-NULL entry in code_watchers
uint8_t active_code_watchers;

struct _Py_unicode_state unicode;
struct _Py_float_state float_state;
Expand Down
68 changes: 68 additions & 0 deletions Lib/test/test_capi/test_watchers.py
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,74 @@ def test_no_more_ids_available(self):
self.add_watcher()


class TestCodeObjectWatchers(unittest.TestCase):
@contextmanager
def code_watcher(self, which_watcher):
wid = _testcapi.add_code_watcher(which_watcher)
try:
yield wid
finally:
_testcapi.clear_code_watcher(wid)

def assert_event_counts(self, exp_created_0, exp_destroyed_0,
exp_created_1, exp_destroyed_1):
self.assertEqual(
exp_created_0, _testcapi.get_code_watcher_num_created_events(0))
self.assertEqual(
exp_destroyed_0, _testcapi.get_code_watcher_num_destroyed_events(0))
self.assertEqual(
exp_created_1, _testcapi.get_code_watcher_num_created_events(1))
self.assertEqual(
exp_destroyed_1, _testcapi.get_code_watcher_num_destroyed_events(1))

def test_code_object_events_dispatched(self):
# verify that all counts are zero before any watchers are registered
self.assert_event_counts(0, 0, 0, 0)

# verify that all counts remain zero when a code object is
# created and destroyed with no watchers registered
co1 = _testcapi.code_newempty("test_watchers", "dummy1", 0)
self.assert_event_counts(0, 0, 0, 0)
del co1
self.assert_event_counts(0, 0, 0, 0)

# verify counts are as expected when first watcher is registered
with self.code_watcher(0):
self.assert_event_counts(0, 0, 0, 0)
co2 = _testcapi.code_newempty("test_watchers", "dummy2", 0)
self.assert_event_counts(1, 0, 0, 0)
del co2
self.assert_event_counts(1, 1, 0, 0)

# again with second watcher registered
with self.code_watcher(1):
self.assert_event_counts(1, 1, 0, 0)
co3 = _testcapi.code_newempty("test_watchers", "dummy3", 0)
self.assert_event_counts(2, 1, 1, 0)
del co3
self.assert_event_counts(2, 2, 1, 1)

# verify counts remain as they were after both watchers are cleared
co4 = _testcapi.code_newempty("test_watchers", "dummy4", 0)
self.assert_event_counts(2, 2, 1, 1)
del co4
self.assert_event_counts(2, 2, 1, 1)

def test_clear_out_of_range_watcher_id(self):
with self.assertRaisesRegex(ValueError, r"Invalid code watcher ID -1"):
_testcapi.clear_code_watcher(-1)
with self.assertRaisesRegex(ValueError, r"Invalid code watcher ID 8"):
_testcapi.clear_code_watcher(8) # CODE_MAX_WATCHERS = 8

def test_clear_unassigned_watcher_id(self):
with self.assertRaisesRegex(ValueError, r"No code watcher set for ID 1"):
_testcapi.clear_code_watcher(1)

def test_allocate_too_many_watchers(self):
with self.assertRaisesRegex(RuntimeError, r"no more code watcher IDs available"):
_testcapi.allocate_too_many_code_watchers()


class TestFuncWatchers(unittest.TestCase):
@contextmanager
def add_watcher(self, func):
Expand Down
2 changes: 2 additions & 0 deletions Misc/ACKS
Original file line number Diff line number Diff line change
Expand Up @@ -1320,6 +1320,7 @@ Michele Orrù
Tomáš Orsava
Oleg Oshmyan
Denis Osipov
Itamar Ostricher
Denis S. Otkidach
Peter Otten
Michael Otteneder
Expand Down Expand Up @@ -1627,6 +1628,7 @@ Silas Sewell
Ian Seyer
Dmitry Shachnev
Anish Shah
Jaineel Shah
Daniel Shahaf
Hui Shang
Geoff Shannon
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Add :c:func:`PyCode_AddWatcher` and :c:func:`PyCode_ClearWatcher` APIs to
register callbacks to receive notification on creation and destruction of
code objects.
131 changes: 131 additions & 0 deletions Modules/_testcapi/watchers.c
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

#define Py_BUILD_CORE
#include "pycore_function.h" // FUNC_MAX_WATCHERS
#include "pycore_code.h" // CODE_MAX_WATCHERS

// Test dict watching
static PyObject *g_dict_watch_events;
Expand Down Expand Up @@ -277,6 +278,126 @@ unwatch_type(PyObject *self, PyObject *args)
Py_RETURN_NONE;
}


// Test code object watching

#define NUM_CODE_WATCHERS 2
static int num_code_object_created_events[NUM_CODE_WATCHERS] = {0, 0};
static int num_code_object_destroyed_events[NUM_CODE_WATCHERS] = {0, 0};

static int
handle_code_object_event(int which_watcher, PyCodeEvent event, PyCodeObject *co) {
if (event == PY_CODE_EVENT_CREATE) {
num_code_object_created_events[which_watcher]++;
}
else if (event == PY_CODE_EVENT_DESTROY) {
num_code_object_destroyed_events[which_watcher]++;
}
else {
return -1;
}
return 0;
}

static int
first_code_object_callback(PyCodeEvent event, PyCodeObject *co)
{
return handle_code_object_event(0, event, co);
}

static int
second_code_object_callback(PyCodeEvent event, PyCodeObject *co)
{
return handle_code_object_event(1, event, co);
}

static int
noop_code_event_handler(PyCodeEvent event, PyCodeObject *co)
{
return 0;
}

static PyObject *
add_code_watcher(PyObject *self, PyObject *which_watcher)
{
int watcher_id;
assert(PyLong_Check(which_watcher));
long which_l = PyLong_AsLong(which_watcher);
if (which_l == 0) {
watcher_id = PyCode_AddWatcher(first_code_object_callback);
}
else if (which_l == 1) {
watcher_id = PyCode_AddWatcher(second_code_object_callback);
}
else {
return NULL;
}
if (watcher_id < 0) {
return NULL;
}
return PyLong_FromLong(watcher_id);
}

static PyObject *
clear_code_watcher(PyObject *self, PyObject *watcher_id)
{
assert(PyLong_Check(watcher_id));
long watcher_id_l = PyLong_AsLong(watcher_id);
if (PyCode_ClearWatcher(watcher_id_l) < 0) {
return NULL;
}
Py_RETURN_NONE;
}

static PyObject *
get_code_watcher_num_created_events(PyObject *self, PyObject *watcher_id)
{
assert(PyLong_Check(watcher_id));
long watcher_id_l = PyLong_AsLong(watcher_id);
assert(watcher_id_l >= 0 && watcher_id_l < NUM_CODE_WATCHERS);
return PyLong_FromLong(num_code_object_created_events[watcher_id_l]);
}

static PyObject *
get_code_watcher_num_destroyed_events(PyObject *self, PyObject *watcher_id)
{
assert(PyLong_Check(watcher_id));
long watcher_id_l = PyLong_AsLong(watcher_id);
assert(watcher_id_l >= 0 && watcher_id_l < NUM_CODE_WATCHERS);
return PyLong_FromLong(num_code_object_destroyed_events[watcher_id_l]);
}

static PyObject *
allocate_too_many_code_watchers(PyObject *self, PyObject *args)
{
int watcher_ids[CODE_MAX_WATCHERS + 1];
int num_watchers = 0;
for (unsigned long i = 0; i < sizeof(watcher_ids) / sizeof(int); i++) {
int watcher_id = PyCode_AddWatcher(noop_code_event_handler);
if (watcher_id == -1) {
break;
}
watcher_ids[i] = watcher_id;
num_watchers++;
}
PyObject *type, *value, *traceback;
PyErr_Fetch(&type, &value, &traceback);
for (int i = 0; i < num_watchers; i++) {
if (PyCode_ClearWatcher(watcher_ids[i]) < 0) {
PyErr_WriteUnraisable(Py_None);
break;
}
}
if (type) {
PyErr_Restore(type, value, traceback);
return NULL;
}
else if (PyErr_Occurred()) {
return NULL;
}
Py_RETURN_NONE;
}

// Test function watchers

#define NUM_FUNC_WATCHERS 2
Expand Down Expand Up @@ -509,6 +630,16 @@ static PyMethodDef test_methods[] = {
{"unwatch_type", unwatch_type, METH_VARARGS, NULL},
{"get_type_modified_events", get_type_modified_events, METH_NOARGS, NULL},

// Code object watchers.
{"add_code_watcher", add_code_watcher, METH_O, NULL},
{"clear_code_watcher", clear_code_watcher, METH_O, NULL},
{"get_code_watcher_num_created_events",
get_code_watcher_num_created_events, METH_O, NULL},
{"get_code_watcher_num_destroyed_events",
get_code_watcher_num_destroyed_events, METH_O, NULL},
{"allocate_too_many_code_watchers",
(PyCFunction) allocate_too_many_code_watchers, METH_NOARGS, NULL},

// Function watchers.
{"add_func_watcher", add_func_watcher, METH_O, NULL},
{"clear_func_watcher", clear_func_watcher, METH_O, NULL},
Expand Down
Loading

0 comments on commit 3c137dc

Please sign in to comment.