diff --git a/Include/cpython/pyframe.h b/Include/cpython/pyframe.h index 51529763923ec3..b403d39b9a9dd0 100644 --- a/Include/cpython/pyframe.h +++ b/Include/cpython/pyframe.h @@ -40,6 +40,7 @@ PyAPI_FUNC(int) PyUnstable_InterpreterFrame_GetLine(struct _PyInterpreterFrame * #define PyUnstable_EXECUTABLE_KIND_PY_FUNCTION 1 #define PyUnstable_EXECUTABLE_KIND_BUILTIN_FUNCTION 3 #define PyUnstable_EXECUTABLE_KIND_METHOD_DESCRIPTOR 4 -#define PyUnstable_EXECUTABLE_KINDS 5 +#define PyUnstable_EXECUTABLE_KIND_JIT 5 +#define PyUnstable_EXECUTABLE_KINDS 6 -PyAPI_DATA(const PyTypeObject *) const PyUnstable_ExecutableKinds[PyUnstable_EXECUTABLE_KINDS+1]; +PyAPI_DATA(PyTypeObject *) PyUnstable_ExecutableKinds[PyUnstable_EXECUTABLE_KINDS+1]; diff --git a/Include/internal/pycore_debug_offsets.h b/Include/internal/pycore_debug_offsets.h index 66f14e69f33f44..f54df38a0065cf 100644 --- a/Include/internal/pycore_debug_offsets.h +++ b/Include/internal/pycore_debug_offsets.h @@ -131,6 +131,10 @@ typedef struct _Py_DebugOffsets { uint64_t tlbc_index; } interpreter_frame; + struct _interpreter_frame_metadata { + uintptr_t executable_kinds; + } interpreter_frame_metadata; + // Code object offset; struct _code_object { uint64_t size; @@ -146,6 +150,10 @@ typedef struct _Py_DebugOffsets { uint64_t co_tlbc; } code_object; + struct _jit_executable { + uint64_t code; + } jit_executable; + // PyObject offset; struct _pyobject { uint64_t size; @@ -305,6 +313,9 @@ typedef struct _Py_DebugOffsets { .stackpointer = offsetof(_PyInterpreterFrame, stackpointer), \ .tlbc_index = _Py_Debug_interpreter_frame_tlbc_index, \ }, \ + .interpreter_frame_metadata = { \ + .executable_kinds = (uintptr_t)PyUnstable_ExecutableKinds, \ + }, \ .code_object = { \ .size = sizeof(PyCodeObject), \ .filename = offsetof(PyCodeObject, co_filename), \ @@ -318,6 +329,9 @@ typedef struct _Py_DebugOffsets { .co_code_adaptive = offsetof(PyCodeObject, co_code_adaptive), \ .co_tlbc = _Py_Debug_code_object_co_tlbc, \ }, \ + .jit_executable = { \ + .code = offsetof(PyUnstable_PyJitExecutable, je_code), \ + }, \ .pyobject = { \ .size = sizeof(PyObject), \ .ob_type = offsetof(PyObject, ob_type), \ diff --git a/Include/internal/pycore_interpframe.h b/Include/internal/pycore_interpframe.h index 8949d6cc2fc4bb..99c1e25bd1d004 100644 --- a/Include/internal/pycore_interpframe.h +++ b/Include/internal/pycore_interpframe.h @@ -17,9 +17,29 @@ extern "C" { #define _PyInterpreterFrame_LASTI(IF) \ ((int)((IF)->instr_ptr - _PyFrame_GetBytecode((IF)))) +PyAPI_DATA(PyTypeObject) PyUnstable_JITExecutable_Type; + +#define PyUnstable_JITExecutable_Check(op) Py_IS_TYPE((op), &PyUnstable_JITExecutable_Type) + +PyAPI_FUNC(PyObject *) PyUnstable_MakeJITExecutable(_PyFrame_Reifier reifier, PyCodeObject *code, PyObject *state); + +PyAPI_FUNC(int) _PyFrame_InitializeExternalFrame(_PyInterpreterFrame *frame); + +static inline int +_PyFrame_EnsureFrameFullyInitialized(_PyInterpreterFrame *frame) +{ + if (PyUnstable_JITExecutable_Check(PyStackRef_AsPyObjectBorrow(frame->f_executable))) { + return _PyFrame_InitializeExternalFrame(frame); + } + return 0; +} + static inline PyCodeObject *_PyFrame_GetCode(_PyInterpreterFrame *f) { assert(!PyStackRef_IsNull(f->f_executable)); PyObject *executable = PyStackRef_AsPyObjectBorrow(f->f_executable); + if (PyUnstable_JITExecutable_Check(executable)) { + return ((PyUnstable_PyJitExecutable *)executable)->je_code; + } assert(PyCode_Check(executable)); return (PyCodeObject *)executable; } @@ -48,6 +68,12 @@ _PyFrame_SafeGetCode(_PyInterpreterFrame *f) if (_PyObject_IsFreed(executable)) { return NULL; } + if (PyUnstable_JITExecutable_Check(executable)) { + executable = (PyObject *)((PyUnstable_PyJitExecutable *)executable)->je_code; + if (_PyObject_IsFreed(executable)) { + return NULL; + } + } if (!PyCode_Check(executable)) { return NULL; } @@ -58,6 +84,10 @@ static inline _Py_CODEUNIT * _PyFrame_GetBytecode(_PyInterpreterFrame *f) { #ifdef Py_GIL_DISABLED + if (_PyFrame_EnsureFrameFullyInitialized(f) < 0) { + return NULL; + } + PyCodeObject *co = _PyFrame_GetCode(f); _PyCodeArray *tlbc = _PyCode_GetTLBCArray(co); assert(f->tlbc_index >= 0 && f->tlbc_index < tlbc->size); @@ -80,6 +110,10 @@ _PyFrame_SafeGetLasti(struct _PyInterpreterFrame *f) return -1; } + if (_PyFrame_EnsureFrameFullyInitialized(f) < 0) { + return -1; + } + _Py_CODEUNIT *bytecode; #ifdef Py_GIL_DISABLED _PyCodeArray *tlbc = _PyCode_GetTLBCArray(co); @@ -277,6 +311,22 @@ _PyThreadState_GetFrame(PyThreadState *tstate) return _PyFrame_GetFirstComplete(tstate->current_frame); } +static inline PyObject * +_PyFrame_GetGlobals(_PyInterpreterFrame *frame) { + if (_PyFrame_EnsureFrameFullyInitialized(frame) < 0) { + return NULL; + } + return frame->f_globals; +} + +static inline PyObject * +_PyFrame_GetBuiltins(_PyInterpreterFrame *frame) { + if (_PyFrame_EnsureFrameFullyInitialized(frame) < 0) { + return NULL; + } + return frame->f_builtins; +} + /* For use by _PyFrame_GetFrameObject Do not call directly. */ PyFrameObject * @@ -288,6 +338,9 @@ _PyFrame_MakeAndSetFrameObject(_PyInterpreterFrame *frame); static inline PyFrameObject * _PyFrame_GetFrameObject(_PyInterpreterFrame *frame) { + if (PyUnstable_JITExecutable_Check(PyStackRef_AsPyObjectBorrow(frame->f_executable))) { + return _PyFrame_MakeAndSetFrameObject(frame); + } assert(!_PyFrame_IsIncomplete(frame)); PyFrameObject *res = frame->frame_obj; @@ -309,7 +362,7 @@ _PyFrame_ClearLocals(_PyInterpreterFrame *frame); * take should be set to 1 for heap allocated * frames like the ones in generators and coroutines. */ -void +PyAPI_FUNC(void) _PyFrame_ClearExceptCode(_PyInterpreterFrame * frame); int diff --git a/Include/internal/pycore_interpframe_structs.h b/Include/internal/pycore_interpframe_structs.h index 38510685f4093c..139b0bbf8e4cf5 100644 --- a/Include/internal/pycore_interpframe_structs.h +++ b/Include/internal/pycore_interpframe_structs.h @@ -85,6 +85,15 @@ struct _PyAsyncGenObject { _PyGenObject_HEAD(ag) }; +typedef int (*_PyFrame_Reifier)(struct _PyInterpreterFrame *, PyObject *reifier); + +typedef struct { + PyObject_HEAD + PyCodeObject *je_code; + PyObject *je_state; + _PyFrame_Reifier je_reifier; +} PyUnstable_PyJitExecutable; + #undef _PyGenObject_HEAD diff --git a/Lib/test/test_capi/test_misc.py b/Lib/test/test_capi/test_misc.py index 3997acbdf84695..0b95cfc14dcd7e 100644 --- a/Lib/test/test_capi/test_misc.py +++ b/Lib/test/test_capi/test_misc.py @@ -4,6 +4,7 @@ import _thread from collections import deque import contextlib +import dis import importlib.machinery import importlib.util import json @@ -2857,6 +2858,85 @@ def func(): names = ["func", "outer", "outer", "inner", "inner", "outer", "inner"] self.do_test(func, names) + def test_jit_frame(self): + def fakefunc(): + pass + + def f(): + return sys._getframe(1) + + res = _testinternalcapi.call_with_jit_frame(fakefunc, f, ()) + + def test_jit_frame_globals(self): + """jit executable can fill in globals when accessed""" + def fakefunc(): + pass + + fake_globals = {"abc":42} + def callback(): + return {"globals": fake_globals} + + res = _testinternalcapi.call_with_jit_frame(fakefunc, globals, (), callback) + self.assertEqual(res, fake_globals) + + def test_jit_frame_builtins(self): + """jit executable can fill in builtins when accessed""" + def fakefunc(): + pass + + fake_builtins = {"abc":42} + def callback(): + return {"builtins": fake_builtins} + + res = _testinternalcapi.call_with_jit_frame(fakefunc, _testlimitedcapi.eval_getbuiltins, (), callback) + self.assertEqual(res, fake_builtins) + + def test_jit_frame_instr_ptr(self): + """jit executable can fill in the instr ptr each time the frame is queried""" + def fakefunc(): + pass + pass + pass + pass + + offset = 0 + linenos = [] + def test(): + for op in dis.get_instructions(fakefunc): + if op.opname in ("RESUME", "NOP", "RETURN_VALUE"): + nonlocal offset + offset = op.offset//2 + linenos.append(sys._getframe(1).f_lineno) + + def callback(): + return {"instr_ptr": offset} + + _testinternalcapi.call_with_jit_frame(fakefunc, test, (), callback) + base = fakefunc.__code__.co_firstlineno + self.assertEqual(linenos, [base, base + 1, base + 2, base + 3, base + 4]) + + def test_jit_frame_code(self): + """internal C api checks the for a code executor""" + def fakefunc(): + pass + + def callback(): + return _testinternalcapi.iframe_getcode(sys._getframe(1)) + + res = _testinternalcapi.call_with_jit_frame(fakefunc, callback, ()) + self.assertEqual(res, fakefunc.__code__) + + def test_jit_frame_line(self): + """internal C api checks the for a code executor""" + def fakefunc(): + pass + + def callback(): + return _testinternalcapi.iframe_getline(sys._getframe(1)) + + res = _testinternalcapi.call_with_jit_frame(fakefunc, callback, ()) + self.assertEqual(res, fakefunc.__code__.co_firstlineno) + @unittest.skipUnless(support.Py_GIL_DISABLED, 'need Py_GIL_DISABLED') class TestPyThreadId(unittest.TestCase): diff --git a/Lib/test/test_external_inspection.py b/Lib/test/test_external_inspection.py index 4f3beb15f53b33..ec0676d540c6f5 100644 --- a/Lib/test/test_external_inspection.py +++ b/Lib/test/test_external_inspection.py @@ -3534,5 +3534,89 @@ def test_get_stats_disabled_raises(self): client_socket.sendall(b"done") +class TestNonCodeExecutable(RemoteInspectionTestBase): + @skip_if_not_supported + @unittest.skipIf( + sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED, + "Test only runs on Linux with process_vm_readv support", + ) + def test_remote_stack_trace(self): + port = find_unused_port() + script = textwrap.dedent( + f"""\ + import time, sys, socket, threading + import _testinternalcapi + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect(('localhost', {port})) + + def bar(): + for x in range(100): + if x == 50: + _testinternalcapi.call_with_jit_frame(baz, foo, ()) + + def baz(): + pass + + def foo(): + sock.sendall(b"ready:thread\\n"); time.sleep(10_000) + + t = threading.Thread(target=bar) + t.start() + sock.sendall(b"ready:main\\n"); t.join() + """ + ) + + with os_helper.temp_dir() as work_dir: + script_dir = os.path.join(work_dir, "script_pkg") + os.mkdir(script_dir) + + server_socket = _create_server_socket(port) + script_name = _make_test_script(script_dir, "script", script) + client_socket = None + + try: + with _managed_subprocess([sys.executable, script_name]) as p: + client_socket, _ = server_socket.accept() + server_socket.close() + server_socket = None + + _wait_for_signal( + client_socket, [b"ready:main", b"ready:thread"] + ) + + try: + stack_trace = get_stack_trace(p.pid) + except PermissionError: + self.skipTest( + "Insufficient permissions to read the stack trace" + ) + + # Find expected thread stack by funcname + found_thread = self._find_thread_with_frame( + stack_trace, + lambda f: f.funcname == "foo" and f.location.lineno == 15, + ) + self.assertIsNotNone( + found_thread, "Expected thread stack trace not found" + ) + # Check the funcnames in order + funcnames = [f.funcname for f in found_thread.frame_info] + self.assertEqual( + funcnames[:6], + ["foo", "baz", "bar", "Thread.run", "Thread._bootstrap_inner", "Thread._bootstrap"] + ) + + # Check main thread + found_main = self._find_frame_in_trace( + stack_trace, + lambda f: f.funcname == "" and f.location.lineno == 19, + ) + self.assertIsNotNone( + found_main, "Main thread stack trace not found" + ) + finally: + _cleanup_sockets(client_socket, server_socket) + + if __name__ == "__main__": unittest.main() diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-12-11-20-23-56.gh-issue-142598.ftYjy7.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-12-11-20-23-56.gh-issue-142598.ftYjy7.rst new file mode 100644 index 00000000000000..61bab81ec2bb48 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-12-11-20-23-56.gh-issue-142598.ftYjy7.rst @@ -0,0 +1 @@ +Adds a new executable type for _PyInterpreterFrame.f_executable PEP 523 JITs to plug in and have their frames visible to external introspection tools. diff --git a/Modules/_remote_debugging/_remote_debugging.h b/Modules/_remote_debugging/_remote_debugging.h index fcb75b841b742e..74d05c13ed70c3 100644 --- a/Modules/_remote_debugging/_remote_debugging.h +++ b/Modules/_remote_debugging/_remote_debugging.h @@ -238,6 +238,7 @@ typedef struct { RemoteDebuggingState *cached_state; FrameCacheEntry *frame_cache; // preallocated array of FRAME_CACHE_MAX_THREADS entries UnwinderStats stats; // statistics for performance analysis + uintptr_t frame_executable_types[PyUnstable_EXECUTABLE_KINDS]; #ifdef Py_GIL_DISABLED uint32_t tlbc_generation; _Py_hashtable_t *tlbc_cache; @@ -332,7 +333,7 @@ extern long read_py_long(RemoteUnwinderObject *unwinder, uintptr_t address); * CODE OBJECT FUNCTION DECLARATIONS * ============================================================================ */ -extern int parse_code_object( +extern int parse_executable_object( RemoteUnwinderObject *unwinder, PyObject **result, uintptr_t address, @@ -473,6 +474,8 @@ extern int populate_initial_state_data( uintptr_t *tstate ); +extern int populate_frame_executable_types(RemoteUnwinderObject *unwinder); + extern int find_running_frame( RemoteUnwinderObject *unwinder, uintptr_t address_of_thread, diff --git a/Modules/_remote_debugging/code_objects.c b/Modules/_remote_debugging/code_objects.c index 98fe74e8cb6331..36c24a1b122a23 100644 --- a/Modules/_remote_debugging/code_objects.c +++ b/Modules/_remote_debugging/code_objects.c @@ -275,12 +275,12 @@ make_frame_info(RemoteUnwinderObject *unwinder, PyObject *file, PyObject *locati } int -parse_code_object(RemoteUnwinderObject *unwinder, - PyObject **result, - uintptr_t address, - uintptr_t instruction_pointer, - uintptr_t *previous_frame, - int32_t tlbc_index) +parse_executable_object(RemoteUnwinderObject *unwinder, + PyObject **result, + uintptr_t address, + uintptr_t instruction_pointer, + uintptr_t *previous_frame, + int32_t tlbc_index) { void *key = (void *)address; CachedCodeMetadata *meta = NULL; @@ -314,6 +314,23 @@ parse_code_object(RemoteUnwinderObject *unwinder, goto error; } + // Check for non code type executables + uintptr_t code_type = GET_MEMBER(uintptr_t, code_object, unwinder->debug_offsets.pyobject.ob_type); + if (code_type != unwinder->frame_executable_types[PyUnstable_EXECUTABLE_KIND_PY_FUNCTION]) { + if (code_type != unwinder->frame_executable_types[PyUnstable_EXECUTABLE_KIND_JIT]) { + // Unsupported executable type, report the frame as being invalid + return 0; + } + real_address = GET_MEMBER(uintptr_t, code_object, unwinder->debug_offsets.jit_executable.code); + if (_Py_RemoteDebug_PagedReadRemoteMemory( + &unwinder->handle, real_address, SIZEOF_CODE_OBJ, code_object) < 0) + { + set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read code object"); + goto error; + } + } + + func = read_py_str(unwinder, GET_MEMBER(uintptr_t, code_object, unwinder->debug_offsets.code_object.qualname), 1024); if (!func) { @@ -364,6 +381,7 @@ parse_code_object(RemoteUnwinderObject *unwinder, ptrdiff_t addrq; #ifdef Py_GIL_DISABLED + real_address = meta->addr_code_adaptive - (uintptr_t)unwinder->debug_offsets.code_object.co_code_adaptive; // Handle thread-local bytecode (TLBC) in free threading builds if (tlbc_index == 0 || unwinder->debug_offsets.code_object.co_tlbc == 0 || unwinder == NULL) { // No TLBC or no unwinder - use main bytecode directly diff --git a/Modules/_remote_debugging/frames.c b/Modules/_remote_debugging/frames.c index abde60c45766a5..b92fc1f8edf32f 100644 --- a/Modules/_remote_debugging/frames.c +++ b/Modules/_remote_debugging/frames.c @@ -11,6 +11,26 @@ * STACK CHUNK MANAGEMENT FUNCTIONS * ============================================================================ */ +int +populate_frame_executable_types(RemoteUnwinderObject *unwinder) +{ + uintptr_t executable_kinds_addr = + (uintptr_t)unwinder->debug_offsets.interpreter_frame_metadata.executable_kinds; + + int bytes_read = _Py_RemoteDebug_PagedReadRemoteMemory( + &unwinder->handle, + executable_kinds_addr, + sizeof(uintptr_t) * PyUnstable_EXECUTABLE_KINDS, + (void *)unwinder->frame_executable_types); + + if (bytes_read < 0) { + set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read frame executable types"); + return -1; + } + + return 0; +} + void cleanup_stack_chunks(StackChunkList *chunks) { @@ -209,7 +229,7 @@ parse_frame_object( #endif *address_of_code_object = code_object; - return parse_code_object(unwinder, result, code_object, instruction_pointer, previous_frame, tlbc_index); + return parse_executable_object(unwinder, result, code_object, instruction_pointer, previous_frame, tlbc_index); } int @@ -246,7 +266,7 @@ parse_frame_from_chunks( } #endif - return parse_code_object(unwinder, result, code_object, instruction_pointer, previous_frame, tlbc_index); + return parse_executable_object(unwinder, result, code_object, instruction_pointer, previous_frame, tlbc_index); } /* ============================================================================ diff --git a/Modules/_remote_debugging/module.c b/Modules/_remote_debugging/module.c index a194d88c3c3ca0..542420706af90d 100644 --- a/Modules/_remote_debugging/module.c +++ b/Modules/_remote_debugging/module.c @@ -370,6 +370,12 @@ _remote_debugging_RemoteUnwinder___init___impl(RemoteUnwinderObject *self, return -1; } + if (populate_frame_executable_types(self) < 0) + { + set_exception_cause(self, PyExc_RuntimeError, "Failed to populate initial state data"); + return -1; + } + self->code_object_cache = _Py_hashtable_new_full( _Py_hashtable_hash_ptr, _Py_hashtable_compare_direct, diff --git a/Modules/_testinternalcapi.c b/Modules/_testinternalcapi.c index 4140cd23ded95e..f64c052dd5276b 100644 --- a/Modules/_testinternalcapi.c +++ b/Modules/_testinternalcapi.c @@ -28,6 +28,7 @@ #include "pycore_initconfig.h" // _Py_GetConfigsAsDict() #include "pycore_instruction_sequence.h" // _PyInstructionSequence_New() #include "pycore_interpframe.h" // _PyFrame_GetFunction() +#include "pycore_interpframe_structs.h" // _PyInterpreterFrame #include "pycore_object.h" // _PyObject_IsFreed() #include "pycore_optimizer.h" // _Py_Executor_DependsOn #include "pycore_pathconfig.h" // _PyPathConfig_ClearGlobal() @@ -693,6 +694,113 @@ set_eval_frame_record(PyObject *self, PyObject *list) Py_RETURN_NONE; } +typedef struct { + bool initialized; + _PyInterpreterFrame frame; +} JitFrame; + +int +reifier(_PyInterpreterFrame *frame, PyObject *executable) +{ + JitFrame *jitframe = (JitFrame*)((char *)frame - offsetof(JitFrame, frame)); + PyFunctionObject *func = (PyFunctionObject *)PyStackRef_AsPyObjectBorrow(frame->f_funcobj); + if (!jitframe->initialized) { + frame->f_locals = NULL; + frame->f_globals = func->func_globals; // borrowed + frame->f_builtins = func->func_builtins; // borrowed + frame->frame_obj = NULL; + jitframe->initialized = true; + } + PyUnstable_PyJitExecutable *jit_exec = (PyUnstable_PyJitExecutable*)executable; + if (jit_exec->je_state == NULL) { + return 0; + } + + PyObject *res = PyObject_CallNoArgs(jit_exec->je_state); + if (res == NULL) { + return -1; + } + + // let the test-state function fill in details on the frame + if (PyDict_Check(res)) { + PyObject *globals = PyDict_GetItemString(res, "globals"); + if (globals != NULL) { + frame->f_globals = globals; + } + PyObject *builtins = PyDict_GetItemString(res, "builtins"); + if (builtins != NULL) { + frame->f_builtins = builtins; + } + PyObject *instr_ptr = PyDict_GetItemString(res, "instr_ptr"); + if (instr_ptr != NULL) { + frame->instr_ptr = _PyCode_CODE((PyCodeObject *)func->func_code) + + PyLong_AsLong(instr_ptr); + } + } + Py_DECREF(res); + return 0; +} + +static PyObject * +call_with_jit_frame(PyObject *self, PyObject *args) +{ + PyObject *fakefunc; // used for f_funcobj as-if we were that JITed function + PyObject *call; // the thing to call for testing purposes + PyObject *callargs; // the arguments to provide for the test call + PyObject *state = NULL; // a state object provided to the reifier, for tests we + // callback on it to populate fields. + if (!PyArg_ParseTuple(args, "OOO|O", &fakefunc, &call, &callargs, &state)) { + return NULL; + } + if (!PyTuple_Check(callargs)) { + PyErr_SetString(PyExc_TypeError, "callargs must be a tuple"); + return NULL; + } + + PyThreadState *tstate = PyThreadState_Get(); + PyCodeObject *code = (PyCodeObject *)((PyFunctionObject *)fakefunc)->func_code; + PyObject *executable = PyUnstable_MakeJITExecutable(reifier, code, state); + if (executable == NULL) { + return NULL; + } + + // Create JIT frame and push onto the _PyInterprerFrame stack. + JitFrame frame; + frame.initialized = false; + // Initialize minimal set of fields + frame.frame.previous = tstate->current_frame; + frame.frame.f_executable = PyStackRef_FromPyObjectSteal(executable); + frame.frame.f_funcobj = PyStackRef_FromPyObjectNew(fakefunc); + frame.frame.instr_ptr = _PyCode_CODE(code) + code->_co_firsttraceable; + frame.frame.stackpointer = &frame.frame.localsplus[0]; + frame.frame.owner = FRAME_OWNED_BY_THREAD; +#ifdef Py_GIL_DISABLED + frame.frame.tlbc_index = 0; +#endif + tstate->current_frame = &frame.frame; + + // call the test function + PyObject *res = PyObject_Call(call, callargs, NULL); + + tstate->current_frame = frame.frame.previous; + // the test function may have caused the frame to get reified. + if (frame.initialized && frame.frame.frame_obj != NULL) { + // remove our reifier + PyStackRef_CLOSE(frame.frame.f_executable); + frame.frame.f_executable = PyStackRef_FromPyObjectNew(code); + + // Transfer ownership to the reified frame object + _PyFrame_ClearExceptCode(&frame.frame); + PyStackRef_CLOSE(frame.frame.f_executable); + } + else { + // Pop frame from the stack + PyStackRef_CLOSE(frame.frame.f_executable); + PyStackRef_CLOSE(frame.frame.f_funcobj); + } + return res; +} + /*[clinic input] _testinternalcapi.compiler_cleandoc -> object @@ -2524,6 +2632,7 @@ static PyMethodDef module_functions[] = { {"DecodeLocaleEx", decode_locale_ex, METH_VARARGS}, {"set_eval_frame_default", set_eval_frame_default, METH_NOARGS, NULL}, {"set_eval_frame_record", set_eval_frame_record, METH_O, NULL}, + {"call_with_jit_frame", call_with_jit_frame, METH_VARARGS, NULL}, _TESTINTERNALCAPI_COMPILER_CLEANDOC_METHODDEF _TESTINTERNALCAPI_NEW_INSTRUCTION_SEQUENCE_METHODDEF _TESTINTERNALCAPI_COMPILER_CODEGEN_METHODDEF diff --git a/Objects/frameobject.c b/Objects/frameobject.c index b652973600c17d..d58da9ef869902 100644 --- a/Objects/frameobject.c +++ b/Objects/frameobject.c @@ -2286,6 +2286,10 @@ _PyFrame_HasHiddenLocals(_PyInterpreterFrame *frame) PyObject * _PyFrame_GetLocals(_PyInterpreterFrame *frame) { + if (_PyFrame_EnsureFrameFullyInitialized(frame) < 0) { + return NULL; + } + // We should try to avoid creating the FrameObject if possible. // So we check if the frame is a module or class level scope PyCodeObject *co = _PyFrame_GetCode(frame); diff --git a/Objects/object.c b/Objects/object.c index 36a37bb0bbea4d..65c02f9e7c5298 100644 --- a/Objects/object.c +++ b/Objects/object.c @@ -2514,6 +2514,7 @@ static PyTypeObject* static_types[] = { &PyTuple_Type, &PyUnicodeIter_Type, &PyUnicode_Type, + &PyUnstable_JITExecutable_Type, &PyWrapperDescr_Type, &PyZip_Type, &Py_GenericAliasType, diff --git a/Python/ceval.c b/Python/ceval.c index 37679d4cd183c7..3cbd1336be9006 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -3128,7 +3128,7 @@ _PyEval_GetBuiltins(PyThreadState *tstate) { _PyInterpreterFrame *frame = _PyThreadState_GetFrame(tstate); if (frame != NULL) { - return frame->f_builtins; + return _PyFrame_GetBuiltins(frame); } return tstate->interp->builtins; } @@ -3237,7 +3237,7 @@ _PyEval_GetGlobals(PyThreadState *tstate) if (current_frame == NULL) { return NULL; } - return current_frame->f_globals; + return _PyFrame_GetGlobals(current_frame); } PyObject * @@ -3367,7 +3367,7 @@ PyObject* PyEval_GetFrameGlobals(void) if (current_frame == NULL) { return NULL; } - return Py_XNewRef(current_frame->f_globals); + return Py_XNewRef(_PyFrame_GetGlobals(current_frame)); } PyObject* PyEval_GetFrameBuiltins(void) @@ -3477,7 +3477,7 @@ _PyEval_ImportName(PyThreadState *tstate, _PyInterpreterFrame *frame, PyObject *name, PyObject *fromlist, PyObject *level) { PyObject *import_func; - if (PyMapping_GetOptionalItem(frame->f_builtins, &_Py_ID(__import__), &import_func) < 0) { + if (PyMapping_GetOptionalItem(_PyFrame_GetBuiltins(frame), &_Py_ID(__import__), &import_func) < 0) { return NULL; } if (import_func == NULL) { @@ -3485,6 +3485,10 @@ _PyEval_ImportName(PyThreadState *tstate, _PyInterpreterFrame *frame, return NULL; } + if (_PyFrame_EnsureFrameFullyInitialized(frame) < 0) { + return NULL; + } + PyObject *locals = frame->f_locals; if (locals == NULL) { locals = Py_None; diff --git a/Python/frame.c b/Python/frame.c index ce216797e47cda..0807b3e3d6cc0f 100644 --- a/Python/frame.c +++ b/Python/frame.c @@ -20,9 +20,21 @@ _PyFrame_Traverse(_PyInterpreterFrame *frame, visitproc visit, void *arg) PyFrameObject * _PyFrame_MakeAndSetFrameObject(_PyInterpreterFrame *frame) { - assert(frame->frame_obj == NULL); PyObject *exc = PyErr_GetRaisedException(); + if (PyUnstable_JITExecutable_Check(PyStackRef_AsPyObjectBorrow(frame->f_executable))) { + if (_PyFrame_EnsureFrameFullyInitialized(frame) < 0) { + return NULL; + } + + PyFrameObject *res = frame->frame_obj; + if (res != NULL) { + PyErr_SetRaisedException(exc); + return res; + } + } + assert(frame->frame_obj == NULL); + PyFrameObject *f = _PyFrame_New_NoTrack(_PyFrame_GetCode(frame)); if (f == NULL) { Py_XDECREF(exc); @@ -127,11 +139,28 @@ _PyFrame_ClearExceptCode(_PyInterpreterFrame *frame) PyStackRef_CLEAR(frame->f_funcobj); } +// Calls the frame reifier and returns the original function object +int +_PyFrame_InitializeExternalFrame(_PyInterpreterFrame *frame) +{ + PyObject *executor = PyStackRef_AsPyObjectBorrow(frame->f_executable); + if (PyUnstable_JITExecutable_Check(executor)) { + PyUnstable_PyJitExecutable *jit_exec = (PyUnstable_PyJitExecutable *)executor; + return jit_exec->je_reifier(frame, executor); + } + + return 0; +} + /* Unstable API functions */ PyObject * PyUnstable_InterpreterFrame_GetCode(struct _PyInterpreterFrame *frame) { + PyObject *executable = PyStackRef_AsPyObjectBorrow(frame->f_executable); + if (PyUnstable_JITExecutable_Check(executable)) { + return Py_NewRef(((PyUnstable_PyJitExecutable *)executable)->je_code); + } return PyStackRef_AsPyObjectNew(frame->f_executable); } @@ -146,14 +175,84 @@ PyUnstable_InterpreterFrame_GetLasti(struct _PyInterpreterFrame *frame) int _Py_NO_SANITIZE_THREAD PyUnstable_InterpreterFrame_GetLine(_PyInterpreterFrame *frame) { + if (_PyFrame_EnsureFrameFullyInitialized(frame) < 0) { + return -1; + } int addr = _PyInterpreterFrame_LASTI(frame) * sizeof(_Py_CODEUNIT); return PyCode_Addr2Line(_PyFrame_GetCode(frame), addr); } -const PyTypeObject *const PyUnstable_ExecutableKinds[PyUnstable_EXECUTABLE_KINDS+1] = { +static int +jitexecutable_traverse(PyObject *self, visitproc visit, void *arg) +{ + PyUnstable_PyJitExecutable *o = (PyUnstable_PyJitExecutable *)self; + Py_VISIT(o->je_code); + Py_VISIT(o->je_state); + return 0; +} + +static int +jitexecutable_clear(PyObject *self) +{ + PyUnstable_PyJitExecutable *o = (PyUnstable_PyJitExecutable *)self; + Py_CLEAR(o->je_code); + Py_CLEAR(o->je_state); + return 0; +} + +static void +jitexecutable_dealloc(PyObject *self) +{ + PyUnstable_PyJitExecutable *o = (PyUnstable_PyJitExecutable *)self; + PyObject_GC_UnTrack(self); + Py_DECREF(o->je_code); + Py_XDECREF(o->je_state); + Py_TYPE(self)->tp_free(self); +} + +PyTypeObject PyUnstable_JITExecutable_Type = { + PyVarObject_HEAD_INIT(&PyType_Type, 0) + .tp_name = "jit_executable", + .tp_basicsize = sizeof(PyUnstable_PyJitExecutable), + .tp_dealloc = jitexecutable_dealloc, + .tp_traverse = jitexecutable_traverse, + .tp_clear = jitexecutable_clear, + .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_IMMUTABLETYPE | Py_TPFLAGS_HAVE_GC, + .tp_alloc = PyType_GenericAlloc, + .tp_free = PyObject_GC_Del, +}; + +PyObject * +PyUnstable_MakeJITExecutable(_PyFrame_Reifier reifier, PyCodeObject *code, PyObject *state) +{ + if (reifier == NULL) { + PyErr_SetString(PyExc_ValueError, "need reifier"); + return NULL; + } else if (code == NULL) { + PyErr_SetString(PyExc_ValueError, "need code object"); + return NULL; + } + + PyUnstable_PyJitExecutable *jit_exec = PyObject_GC_New(PyUnstable_PyJitExecutable, + &PyUnstable_JITExecutable_Type); + if (jit_exec == NULL) { + return NULL; + } + + jit_exec->je_reifier = reifier; + jit_exec->je_code = (PyCodeObject *)Py_NewRef(code); + jit_exec->je_state = Py_XNewRef(state); + if (state != NULL && PyObject_GC_IsTracked(state)) { + PyObject_GC_Track((PyObject *)jit_exec); + } + return (PyObject *)jit_exec; +} + +PyTypeObject *PyUnstable_ExecutableKinds[PyUnstable_EXECUTABLE_KINDS+1] = { [PyUnstable_EXECUTABLE_KIND_SKIP] = &_PyNone_Type, [PyUnstable_EXECUTABLE_KIND_PY_FUNCTION] = &PyCode_Type, [PyUnstable_EXECUTABLE_KIND_BUILTIN_FUNCTION] = &PyMethod_Type, [PyUnstable_EXECUTABLE_KIND_METHOD_DESCRIPTOR] = &PyMethodDescr_Type, + [PyUnstable_EXECUTABLE_KIND_JIT] = &PyUnstable_JITExecutable_Type, [PyUnstable_EXECUTABLE_KINDS] = NULL, }; diff --git a/Python/tracemalloc.c b/Python/tracemalloc.c index 20351618721c3b..63660794b397b0 100644 --- a/Python/tracemalloc.c +++ b/Python/tracemalloc.c @@ -221,7 +221,6 @@ hashtable_compare_traceback(const void *key1, const void *key2) static void tracemalloc_get_frame(_PyInterpreterFrame *pyframe, frame_t *frame) { - assert(PyStackRef_CodeCheck(pyframe->f_executable)); frame->filename = &_Py_STR(anon_unknown); int lineno = PyUnstable_InterpreterFrame_GetLine(pyframe); diff --git a/Tools/c-analyzer/cpython/globals-to-fix.tsv b/Tools/c-analyzer/cpython/globals-to-fix.tsv index 301784f773d31f..47a2527dc9ab92 100644 --- a/Tools/c-analyzer/cpython/globals-to-fix.tsv +++ b/Tools/c-analyzer/cpython/globals-to-fix.tsv @@ -108,6 +108,7 @@ Python/bltinmodule.c - PyZip_Type - Python/context.c - PyContextToken_Type - Python/context.c - PyContextVar_Type - Python/context.c - PyContext_Type - +Python/frame.c - PyUnstable_JITExecutable_Type - Python/instruction_sequence.c - _PyInstructionSequence_Type - Python/instrumentation.c - _PyLegacyBranchEventHandler_Type - Python/instrumentation.c - _PyBranchesIterator - @@ -324,6 +325,7 @@ Objects/sliceobject.c - _Py_EllipsisObject - Objects/typevarobject.c - _Py_NoDefaultStruct - Python/instrumentation.c - _PyInstrumentation_DISABLE - Python/instrumentation.c - _PyInstrumentation_MISSING - +Python/frame.c - PyUnstable_ExecutableKinds - ################################## diff --git a/Tools/check-c-api-docs/ignored_c_api.txt b/Tools/check-c-api-docs/ignored_c_api.txt index e81ffd51e193b2..2f05521c562e7c 100644 --- a/Tools/check-c-api-docs/ignored_c_api.txt +++ b/Tools/check-c-api-docs/ignored_c_api.txt @@ -81,8 +81,10 @@ PyLong_SHIFT # cpython/pyerrors.h PyException_HEAD # cpython/pyframe.h +PyUnstable_ExecutableKinds PyUnstable_EXECUTABLE_KINDS PyUnstable_EXECUTABLE_KIND_BUILTIN_FUNCTION +PyUnstable_EXECUTABLE_KIND_JIT PyUnstable_EXECUTABLE_KIND_METHOD_DESCRIPTOR PyUnstable_EXECUTABLE_KIND_PY_FUNCTION PyUnstable_EXECUTABLE_KIND_SKIP