Repository navigation
Add new type of executable to support external JITs #142598
Description
Activity
- addedtype-featureA feature request or enhancementA feature request or enhancement
on Dec 11, 2025 - addedinterpreter-core(Objects, Python, Grammar, and Parser dirs)(Objects, Python, Grammar, and Parser dirs)
on Dec 11, 2025 Looking through this, and the associated PR, I don't think this is a viable approach. Too much of the VM assumes that frames exist, are in a complete and consistent state.
What happens when, for example, a local variable is changed in a debugger, or an out-of-date frame is introspected?We have the notion of an incomplete frame, and I would like to have minimal frames, so that C extensions and such could appear in backtraces and profiling information, but those would be complete and consistent.
Lazily completing a frame, in place, might be a viable option though.
Going through the frame, field by field:
We are going to need to do something about the instruction pointer to support the JIT and free-threading: #146208
Thef_globalsand builtins fields can be lazily initialized by the VM, so that a JIT could just set them toNULL
f_localsis almost alwaysNULL, andf_frameobjshould always be initialized toNULL
That leaves the function object (f_funcobj), the code object (f_executable), the locals and stack.Setting the stack pointer to
NULLshould be fine (we currently use aNULLstack pointer as a sentinel in debug mode, but changing it to call a reifier function would work fine, as that could do the check).
If the function object, code object or stack pointer areNULLand they are required, then the reification function would be called.Overall, I make that 6 fields that would be nulled per frame. That can be done in 3 instructions on both Aarch64 and x86-64, if the frame is laid out so that the fields to be NULLed are paired up.
Doing this would change the signature of the reification function from:
_PyInterpreterFrame * reify_frame(_PyInterpreterFrameCore *, PyObject *reifier);
to
int complete_frame(_PyInterpreterFrame *, PyObject *reifier);This might require the jitted code to do more work, but it should integrate a lot more effectively with the rest of the VM and tooling.
@DinoV what do you think?
It sounds like this is getting closer to my original version :) I assume part of the problem with this version is just the amount of dealing w/ core frames vs non-core ones? it really is extreme...
What about something that's more of a hybrid... make f_locals, f_frameobj and instrptr opaque pointers (void * or some type which holds a void * and indicates that they could be uninitialized). Then these get consistently accessed via helpers such as
_PyFrame_GetLocalsthat can always do the right thing. They're so rarely accessed that we should push the cost onto the accessor who can go through the reifier.I could see globals and builtins getting the same treatment but maybe that's too much complexity for something that's more likely to be used - and as you point out the VM can easily handle populating these from the function and it's just initing to NULL.
Currently we're not getting away from initializing the function either (we need it to re-initialize globals/builtins and so would the VM) so that's not much of a change.
So now the only remaining thing is the stack pointer, which is interesting. I think we want to be able to have this called at GC time to be able to dynamically participate w/ GC and deferred reference counting. So maybe that could have similar treatment w/ f_locals?
The values in
f_locals,f_frameobjare accessed during GC, so they will be need to be safe to traverse without initializing. Setting them toNULLshould be fine.The stack pointer needs to valid during GC, but it doesn't need to reflect the canonical frame state at all.
It just need these properties to hold:- All deferred references to objects belonging to the frame must lie between
frame->localsplusandframe->stackpointer - All references in the frame between
frame->localsplusandframe->stackpointermust be legal (NULLand tagged ints are allowed)
I'm guessing you want to set it once to
NULLhave the reifier fill it in, rather than have to set it across every call?- All deferred references to objects belonging to the frame must lie between
Feature or enhancement
Proposal:
#105727 introduced the ability to have different types of executables. During the discussion of that there was mention of potentially adding support for JITs to be able to plug in as well. There's a similar disucssion of the idea over here: faster-cpython/ideas#575
This extends the existing set of executors so that PEP-523 external tools can hook into the interpreter frames and create their own external frames which live on the stack with some minimal amount of initialization. This can be used by JIT compilers, tools that compile Python code to C (e.g. Cython, MyPyC), or other tools that would like to display frames in the Python stack.
The initialization that is required is minimal. There is a new
_PyInterpreterFrameCorestruct that is added which is the first field of the_PyInterpreterFramestruct - this cleanly captures the data which needs initialization and ensures the runtime isn't inadvertently accessing fields of uninitialized frames. The fields of the core structure are: f_executable, previous, and owner. While the frame only has the minimal values initialized the full size of the frame does need to be allocated so that reification doesn't fail - and the reifier is explicitly disallowed from returning a different pointer.The f_executable is initialized to a value which is created by
PyUnstable_MakeExternalExecutableThis is created with a callback that is used to reify the full frame, a code object, and an optional piece of state.The reifier is called in various places where the JIT needs to populate the frame - for example accessing globals, builtins, instr_ptr or creating a user-visible
frameobject. Currently the API just provides a single callback with no information on what's required. Future iterations could modify this to allow finer grained control if it was desired - but reification of the frame is generally going to happen in the slow path so simplicity seems the best here.The code object is provided both for external introspection tools and for the runtime to provide the code object in reified frames. In the external introspection tools case reifier can not be invoked so the code object provides some minimal amount of information about the frame. Because the reifier may want callbacks to update the the frame state over time
f_executablecannot be restored to the code object on first reification. Therefore the code object here also allows the runtime to properly create a user-visible Python frame object for the external frame complete withf_code.The state object is provided for the reifier to provide any state which is necessary to reify the frame assuming it has no where else to store it. For example this could include the function object so that the reifier can populate functions, globals and builtins with simple reads. Another example is it could also include data which maps from IP address to instruction pointer if the reifier wanted to update instr_ptr on each callback. The reifier needs to store sufficient data here or somewhere else so that it can reify the frame without invoking other arbitrary Python code that could cause infinite recursion and can do so without failing.
The runtime will always call the reifier on an external frame before accessing members of that frame. This is captured in the reifier callback API where the reifier is transforming the
_PyInterpreterFrameCoreinto a fully initialized_PyInterpreterFramethat respects all of the guarantees of a standard_PyInterpreterFrame. While this returns a pointer of a different type it is an error to return a pointer to a different address. The JIT doesn't need to continue to maintain the initialized state of the frame and the runtime will always call the reifier before further accesses at a different point in time. The reification must also not fail - the external frame owner must have all of the data required to initialize the frame. The reifier can zero initialize the locals, stackpointer, frame_obj.The
_frameownerenum is updated to note that a frame is externally owned. Technically this can be derived by thef_executabletype but it is simpler to check the owner field. A flag is added to indicate that the frame is externally owned and can be applied to either frames owned by threads or generators. Frames owned by the frame object are never externally owned are neither are frames owned by the interpreter.During GC external frames will not have their fields introspected and the reifier callback will not be invoked.
The runtime will consider the external frame to be "complete" - it will be visible in stack traces. This is because these frames are designed to be user-visible.
Has this already been discussed elsewhere?
This is a minor feature, which does not need previous discussion elsewhere
Links to previous discussion of this feature:
#105727
faster-cpython/ideas#575
Linked PRs