From 426a915349160a44af9a0e374534fc0d8d9655a2 Mon Sep 17 00:00:00 2001 From: SubbaraoGarlapati Date: Tue, 10 Feb 2026 16:11:43 -0500 Subject: [PATCH] gh-144690: Add C API for trace/profile callback registration --- Include/internal/pycore_instruments.h | 13 ++++ Include/internal/pycore_interp_structs.h | 5 ++ Lib/test/test_capi/test_misc.py | 78 +++++++++++++++++++ ...-02-10-16-07-57.gh-issue-144690.tu_xmD.rst | 5 ++ Modules/_testinternalcapi.c | 42 ++++++++++ Python/legacy_tracing.c | 50 ++++++++++++ 6 files changed, 193 insertions(+) create mode 100644 Misc/NEWS.d/next/C_API/2026-02-10-16-07-57.gh-issue-144690.tu_xmD.rst diff --git a/Include/internal/pycore_instruments.h b/Include/internal/pycore_instruments.h index 3775b074ecf54c..9a2d599c5d6115 100644 --- a/Include/internal/pycore_instruments.h +++ b/Include/internal/pycore_instruments.h @@ -123,6 +123,19 @@ typedef struct _PyCoMonitoringData { } _PyCoMonitoringData; +/* Callback API for notifications when sys.settrace/sys.setprofile are called. */ +typedef enum { + PyUnstable_EVAL_TRACE_SET = 0, + PyUnstable_EVAL_TRACE_CLEAR = 1, + PyUnstable_EVAL_PROFILE_SET = 2, + PyUnstable_EVAL_PROFILE_CLEAR = 3, +} PyUnstable_EvalEvent; + +typedef int (*PyUnstable_EvalCallback)(PyUnstable_EvalEvent event, void *data); + +PyAPI_FUNC(int) PyUnstable_SetEvalCallback(PyUnstable_EvalCallback callback, void *data); +PyAPI_FUNC(PyUnstable_EvalCallback) PyUnstable_GetEvalCallback(void **data); + #ifdef __cplusplus } #endif diff --git a/Include/internal/pycore_interp_structs.h b/Include/internal/pycore_interp_structs.h index 723657e4cef10d..59462ab2d03957 100644 --- a/Include/internal/pycore_interp_structs.h +++ b/Include/internal/pycore_interp_structs.h @@ -1006,6 +1006,11 @@ struct _is { #endif #endif + struct { + PyUnstable_EvalCallback callback; + void *data; + } eval_callback; + /* the initial PyInterpreterState.threads.head */ _PyThreadStateImpl _initial_thread; // _initial_thread should be the last field of PyInterpreterState. diff --git a/Lib/test/test_capi/test_misc.py b/Lib/test/test_capi/test_misc.py index 3997acbdf84695..88ea0954f7ddd8 100644 --- a/Lib/test/test_capi/test_misc.py +++ b/Lib/test/test_capi/test_misc.py @@ -2858,6 +2858,84 @@ def func(): self.do_test(func, names) +class TestEvalCallback(unittest.TestCase): + """Test PyUnstable_SetEvalCallback / PyUnstable_GetEvalCallback API""" + + # Event constants matching PyUnstable_EvalEvent enum values + EVAL_TRACE_SET = 0 + EVAL_TRACE_CLEAR = 1 + EVAL_PROFILE_SET = 2 + EVAL_PROFILE_CLEAR = 3 + + def setUp(self): + self.events = [] + _testinternalcapi.set_eval_callback_record(self.events) + + def tearDown(self): + _testinternalcapi.clear_eval_callback() + sys.settrace(None) + sys.setprofile(None) + + def test_settrace_fires_callback(self): + def dummy_trace(frame, event, arg): + return dummy_trace + sys.settrace(dummy_trace) + self.assertIn(self.EVAL_TRACE_SET, self.events) + + def test_settrace_none_fires_clear(self): + def dummy_trace(frame, event, arg): + return dummy_trace + sys.settrace(dummy_trace) + self.events.clear() + sys.settrace(None) + self.assertIn(self.EVAL_TRACE_CLEAR, self.events) + + def test_setprofile_fires_callback(self): + def dummy_profile(frame, event, arg): + pass + sys.setprofile(dummy_profile) + self.assertIn(self.EVAL_PROFILE_SET, self.events) + + def test_setprofile_none_fires_clear(self): + def dummy_profile(frame, event, arg): + pass + sys.setprofile(dummy_profile) + self.events.clear() + sys.setprofile(None) + self.assertIn(self.EVAL_PROFILE_CLEAR, self.events) + + def test_multiple_set_clear_cycles(self): + def dummy_trace(frame, event, arg): + return dummy_trace + def dummy_profile(frame, event, arg): + pass + + sys.settrace(dummy_trace) + sys.settrace(None) + sys.setprofile(dummy_profile) + sys.setprofile(None) + + self.assertEqual(self.events, [ + self.EVAL_TRACE_SET, + self.EVAL_TRACE_CLEAR, + self.EVAL_PROFILE_SET, + self.EVAL_PROFILE_CLEAR, + ]) + + def test_clear_callback_stops_events(self): + _testinternalcapi.clear_eval_callback() + events_after_clear = [] + _testinternalcapi.set_eval_callback_record(events_after_clear) + _testinternalcapi.clear_eval_callback() + + def dummy_trace(frame, event, arg): + return dummy_trace + sys.settrace(dummy_trace) + sys.settrace(None) + + self.assertEqual(events_after_clear, []) + + @unittest.skipUnless(support.Py_GIL_DISABLED, 'need Py_GIL_DISABLED') class TestPyThreadId(unittest.TestCase): def test_py_thread_id(self): diff --git a/Misc/NEWS.d/next/C_API/2026-02-10-16-07-57.gh-issue-144690.tu_xmD.rst b/Misc/NEWS.d/next/C_API/2026-02-10-16-07-57.gh-issue-144690.tu_xmD.rst new file mode 100644 index 00000000000000..e98a34523af886 --- /dev/null +++ b/Misc/NEWS.d/next/C_API/2026-02-10-16-07-57.gh-issue-144690.tu_xmD.rst @@ -0,0 +1,5 @@ +Added :c:func:`PyUnstable_SetEvalCallback` and +:c:func:`PyUnstable_GetEvalCallback` to receive notifications when +:func:`sys.settrace` or :func:`sys.setprofile` are called. This allows JIT +compilers and other tools using :pep:`523` frame evaluation hooks to +efficiently detect tracing/profiling changes without polling. diff --git a/Modules/_testinternalcapi.c b/Modules/_testinternalcapi.c index 22cfa3f58a9d83..c6ad4142e808f5 100644 --- a/Modules/_testinternalcapi.c +++ b/Modules/_testinternalcapi.c @@ -38,6 +38,7 @@ #include "pycore_pystate.h" // _PyThreadState_GET() #include "pycore_runtime_structs.h" // _PY_NSMALLPOSINTS #include "pycore_unicodeobject.h" // _PyUnicode_TransformDecimalAndSpaceToASCII() +#include "pycore_instruments.h" // PyUnstable_SetEvalCallback #include "clinic/_testinternalcapi.c.h" @@ -2836,9 +2837,50 @@ test_threadstate_set_stack_protection(PyObject *self, PyObject *Py_UNUSED(args)) } +// Helper for testing PyUnstable_SetEvalCallback / PyUnstable_GetEvalCallback +static int +test_eval_callback(PyUnstable_EvalEvent event, void *data) +{ + if (data == NULL) { + return 0; + } + PyObject *event_int = PyLong_FromLong((long)event); + if (event_int == NULL) { + return -1; + } + int res = PyList_Append((PyObject *)data, event_int); + Py_DECREF(event_int); + return res; +} + +static PyObject * +set_eval_callback_record(PyObject *self, PyObject *list) +{ + if (!PyList_Check(list)) { + PyErr_SetString(PyExc_TypeError, "argument must be a list"); + return NULL; + } + if (PyUnstable_SetEvalCallback(test_eval_callback, list) < 0) { + return NULL; + } + Py_RETURN_NONE; +} + +static PyObject * +clear_eval_callback(PyObject *self, PyObject *Py_UNUSED(args)) +{ + if (PyUnstable_SetEvalCallback(NULL, NULL) < 0) { + return NULL; + } + Py_RETURN_NONE; +} + + static PyMethodDef module_functions[] = { {"get_configs", get_configs, METH_NOARGS}, {"get_eval_frame_stats", get_eval_frame_stats, METH_NOARGS, NULL}, + {"set_eval_callback_record", set_eval_callback_record, METH_O, NULL}, + {"clear_eval_callback", clear_eval_callback, METH_NOARGS, NULL}, {"get_recursion_depth", get_recursion_depth, METH_NOARGS}, {"get_c_recursion_remaining", get_c_recursion_remaining, METH_NOARGS}, {"get_stack_pointer", get_stack_pointer, METH_NOARGS}, diff --git a/Python/legacy_tracing.c b/Python/legacy_tracing.c index 594d5c5ead5021..6f8d5183a029c1 100644 --- a/Python/legacy_tracing.c +++ b/Python/legacy_tracing.c @@ -7,6 +7,7 @@ #include "pycore_ceval.h" // export _PyEval_SetProfile() #include "pycore_frame.h" // PyFrameObject members #include "pycore_interpframe.h" // _PyFrame_GetCode() +#include "pycore_instruments.h" // PyUnstable_SetEvalCallback #include "opcode.h" #include @@ -521,6 +522,39 @@ set_monitoring_profile_events(PyInterpreterState *interp) return _PyMonitoring_SetEvents(PY_MONITORING_SYS_PROFILE_ID, events); } +int +PyUnstable_SetEvalCallback(PyUnstable_EvalCallback callback, void *data) +{ + PyInterpreterState *interp = _PyInterpreterState_GET(); + interp->eval_callback.callback = callback; + interp->eval_callback.data = data; + return 0; +} + +PyUnstable_EvalCallback +PyUnstable_GetEvalCallback(void **data) +{ + PyInterpreterState *interp = _PyInterpreterState_GET(); + if (data != NULL) { + *data = interp->eval_callback.data; + } + return interp->eval_callback.callback; +} + +static inline void +notify_eval_callback(PyInterpreterState *interp, PyUnstable_EvalEvent event) +{ + if (interp->eval_callback.callback != NULL) { + void *data = interp->eval_callback.data; + if (interp->eval_callback.callback(event, data) < 0) { + PyErr_FormatUnraisable( + "Exception ignored in %s eval callback", + (event == PyUnstable_EVAL_TRACE_SET || event == PyUnstable_EVAL_TRACE_CLEAR) + ? "trace" : "profile"); + } + } +} + int _PyEval_SetProfile(PyThreadState *tstate, Py_tracefunc func, PyObject *arg) { @@ -546,6 +580,10 @@ _PyEval_SetProfile(PyThreadState *tstate, Py_tracefunc func, PyObject *arg) int ret = set_monitoring_profile_events(interp); _PyEval_StartTheWorld(interp); Py_XDECREF(old_profileobj); // needs to be decref'd outside of stop-the-world + + PyUnstable_EvalEvent event = (func != NULL) ? PyUnstable_EVAL_PROFILE_SET : PyUnstable_EVAL_PROFILE_CLEAR; + notify_eval_callback(interp, event); + return ret; } @@ -586,6 +624,10 @@ _PyEval_SetProfileAllThreads(PyInterpreterState *interp, Py_tracefunc func, PyOb int ret = set_monitoring_profile_events(interp); _PyEval_StartTheWorld(interp); Py_XDECREF(old_profileobjs); // needs to be decref'd outside of stop-the-world + + PyUnstable_EvalEvent event = (func != NULL) ? PyUnstable_EVAL_PROFILE_SET : PyUnstable_EVAL_PROFILE_CLEAR; + notify_eval_callback(interp, event); + return ret; } @@ -719,6 +761,10 @@ _PyEval_SetTrace(PyThreadState *tstate, Py_tracefunc func, PyObject *arg) done: _PyEval_StartTheWorld(interp); Py_XDECREF(old_traceobj); // needs to be decref'd outside stop-the-world + + PyUnstable_EvalEvent event = (func != NULL) ? PyUnstable_EVAL_TRACE_SET : PyUnstable_EVAL_TRACE_CLEAR; + notify_eval_callback(interp, event); + return err; } @@ -770,5 +816,9 @@ _PyEval_SetTraceAllThreads(PyInterpreterState *interp, Py_tracefunc func, PyObje int err = set_monitoring_trace_events(interp); _PyEval_StartTheWorld(interp); Py_XDECREF(old_trace_objs); // needs to be decref'd outside of stop-the-world + + PyUnstable_EvalEvent event = (func != NULL) ? PyUnstable_EVAL_TRACE_SET : PyUnstable_EVAL_TRACE_CLEAR; + notify_eval_callback(interp, event); + return err; }