Skip to content
This repository was archived by the owner on Feb 13, 2025. It is now read-only.

Commit 34108b9

Browse files
authored
Stackless issue #275: Support PEP-523 eval_frame-hook
Support the PEP-523 eval_frame-hook for one particular use case: byte code instrumentation. An example is the pydev-debugger. It can use the hook to inject "break-points". This is much more efficient, than traditional trace functions.
1 parent 05f4c34 commit 34108b9

File tree

5 files changed

+223
-9
lines changed

5 files changed

+223
-9
lines changed

Include/internal/pycore_stackless.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -814,6 +814,9 @@ long slp_parse_thread_id(PyObject *thread_id, unsigned long *id);
814814
((frame_)->f_executing >= SLP_FRAME_EXECUTING_VALUE && \
815815
(frame_)->f_executing <= SLP_FRAME_EXECUTING_YIELD_FROM)
816816

817+
/* Frame is executing, ignore value in retval.
818+
* This is used, if the eval_frame hook is in use. */
819+
#define SLP_FRAME_EXECUTING_HOOK ((char)100)
817820

818821
/* Defined in slp_transfer.c */
819822
int

Python/ceval.c

Lines changed: 47 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4171,34 +4171,67 @@ handle_unwinding(int lineno, PyFrameObject *f,
41714171
}
41724172

41734173
PyObject*
4174-
PyEval_EvalFrameEx_slp(PyFrameObject *f, int throwflag, PyObject *retval_arg)
4174+
PyEval_EvalFrameEx_slp(PyFrameObject *f, int throwflag, PyObject *retval)
41754175
{
41764176
PyThreadState *tstate = _PyThreadState_GET();
41774177
int executing = f->f_executing;
41784178
if (executing == SLP_FRAME_EXECUTING_INVALID) {
41794179
--tstate->recursion_depth;
4180-
return slp_cannot_execute((PyCFrameObject *)f, "PyEval_EvalFrameEx_slp", retval_arg);
4181-
} else if (executing == SLP_FRAME_EXECUTING_NO) {
4180+
return slp_cannot_execute((PyCFrameObject *)f, "PyEval_EvalFrameEx_slp", retval);
4181+
} else if (executing == SLP_FRAME_EXECUTING_NO ||
4182+
executing == SLP_FRAME_EXECUTING_HOOK) {
41824183
/* Processing of a frame starts here */
41834184

41844185
/* Check, if an extension module has changed tstate->interp->eval_frame.
41854186
* PEP 523 defines this function pointer as an API to customise the frame
4186-
* evaluation. Stackless can not support this API. In order to prevent
4187-
* undefined behavior, we terminate the interpreter.
4187+
* evaluation. Stackless support this API and calls it, if it starts the
4188+
* evaluation of a frame.
41884189
*/
4189-
if (tstate->interp->eval_frame != _PyEval_EvalFrameDefault)
4190+
if (executing == SLP_FRAME_EXECUTING_NO &&
4191+
tstate->interp->eval_frame != _PyEval_EvalFrameDefault) {
4192+
Py_XDECREF(retval);
4193+
f->f_executing = SLP_FRAME_EXECUTING_HOOK;
4194+
retval = tstate->interp->eval_frame(f, throwflag);
4195+
/* There are two possibilities:
4196+
* - Either the hook-functions delegates to _PyEval_EvalFrameDefault
4197+
* Then the frame transfer protocol is observed, SLP_STORE_NEXT_FRAME(tstate, f->f_back)
4198+
* has been called.
4199+
* - Or the the hook function does not call _PyEval_EvalFrameDefault
4200+
* Then the frame transfer protocol is (probably) violated and the code just
4201+
* assigned tstate->frame = f->f_back.
4202+
*
4203+
* To distinguish both cases, we look a f->f_executing. If the value is still
4204+
* SLP_FRAME_EXECUTING_HOOK, then _PyEval_EvalFrameDefault wasn't called for frame f.
4205+
*/
4206+
if (f->f_executing != SLP_FRAME_EXECUTING_HOOK)
4207+
/* _PyEval_EvalFrameDefault was called */
4208+
return retval;
4209+
/* Try to repair the frame reference count.
4210+
* It is possible in case of a simple tstate->frame = f->f_back */
4211+
if (tstate->frame == f->f_back) {
4212+
SLP_STORE_NEXT_FRAME(tstate, f->f_back);
4213+
return retval;
4214+
}
4215+
/* Game over */
41904216
Py_FatalError("An extension module has set a custom frame evaluation function (see PEP 523).\n"
4191-
"Stackless Python does not support the frame evaluation API defined by PEP 523.\n"
4217+
"Stackless Python does not completely support the frame evaluation API defined by PEP 523.\n"
41924218
"The programm now terminates to prevent undefined behavior.\n");
4219+
} else if (executing == SLP_FRAME_EXECUTING_HOOK) {
4220+
f->f_executing = SLP_FRAME_EXECUTING_NO;
4221+
}
41934222

41944223
if (SLP_CSTACK_SAVE_NOW(tstate, f)) {
41954224
/* Setup the C-stack and recursively call PyEval_EvalFrameEx_slp with the same arguments.
41964225
* SLP_CSTACK_SAVE_NOW(tstate, f) will be false then.
41974226
*/
4198-
return slp_eval_frame_newstack(f, throwflag, retval_arg);
4227+
return slp_eval_frame_newstack(f, throwflag, retval);
41994228
}
42004229
}
4201-
return slp_eval_frame_value(f, throwflag, retval_arg);
4230+
4231+
/* This is the only call of static slp_eval_frame_value.
4232+
* An optimizing compiler will eliminate this call
4233+
*/
4234+
return slp_eval_frame_value(f, throwflag, retval);
42024235
}
42034236

42044237

@@ -4261,6 +4294,7 @@ _PyEval_EvalFrameDefault(PyFrameObject *f, int throwflag)
42614294
}
42624295
if (PyFrame_Check(f)) {
42634296
if (!(f->f_executing == SLP_FRAME_EXECUTING_NO ||
4297+
f->f_executing == SLP_FRAME_EXECUTING_HOOK ||
42644298
SLP_FRAME_IS_EXECUTING(f))) {
42654299
PyErr_BadInternalCall();
42664300
return NULL;
@@ -4282,6 +4316,10 @@ _PyEval_EvalFrameDefault(PyFrameObject *f, int throwflag)
42824316
retval = Py_None;
42834317
}
42844318

4319+
if (f->f_executing == SLP_FRAME_EXECUTING_HOOK)
4320+
/* Used, if a eval_frame-hook is installed */
4321+
return PyEval_EvalFrameEx_slp(f, throwflag, retval);
4322+
42854323
/* test, if the stackless system has been initialized. */
42864324
if (tstate->st.main == NULL) {
42874325
/* Call from extern. Same logic as PyStackless_Call_Main */

Stackless/changelog.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ What's New in Stackless 3.X.X?
99

1010
*Release date: 20XX-XX-XX*
1111

12+
- https://github.com/stackless-dev/stackless/issues/275
13+
Add limited support for the eval_frame-hook introduced by PEP-523.
14+
Stackless can't fully support all use-cases. Currently the hook can be used
15+
by a debugger (i.e. pydevd) to modify the Python byte-code.
16+
1217
- https://github.com/stackless-dev/stackless/issues/297
1318
Stackless now works on Linux ARM64 (architecture "aarch64").
1419

Stackless/module/_teststackless.c

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,92 @@ static PyObject* test_PyEval_EvalFrameEx(PyObject *self, PyObject *args, PyObjec
450450
return result;
451451
}
452452

453+
static PyObject *pep523_frame_hook_throw = NULL;
454+
static PyObject *pep523_frame_hook_store_args = NULL;
455+
static PyObject *pep523_frame_hook_result = NULL;
456+
457+
static PyObject *
458+
pep523_frame_hook_eval_frame(PyFrameObject *f, int throwflag) {
459+
PyThreadState *tstate = PyThreadState_GET();
460+
if (pep523_frame_hook_store_args) {
461+
PyObject *ret = PyObject_CallFunction(pep523_frame_hook_store_args, "((Oi))", (PyObject *)f, throwflag);
462+
if (!ret) {
463+
tstate->frame = f->f_back;
464+
return NULL;
465+
}
466+
Py_DECREF(ret);
467+
}
468+
if (pep523_frame_hook_throw) {
469+
PyErr_SetObject((PyObject *)Py_TYPE(pep523_frame_hook_throw), pep523_frame_hook_throw);
470+
Py_CLEAR(pep523_frame_hook_throw);
471+
tstate->frame = f->f_back;
472+
return NULL;
473+
}
474+
PyObject *retval = _PyEval_EvalFrameDefault(f, throwflag);
475+
476+
/* After _PyEval_EvalFrameDefault the current frame is invalid.
477+
* We must not enter the interpreter and we can't use the frame transfer macros,
478+
* because they are a Py_BUILD_CORE API only.
479+
* In practice entering the interpreter would work. Only if you compile
480+
* Stackless with SLP_WITH_FRAME_REF_DEBUG defined you get assertion failures.
481+
*/
482+
if (retval != NULL ) {
483+
Py_INCREF(retval);
484+
Py_XSETREF(pep523_frame_hook_result, retval);
485+
} else {
486+
Py_INCREF(Py_NotImplemented); /* anything else but None */
487+
Py_XSETREF(pep523_frame_hook_result, Py_NotImplemented);
488+
}
489+
return retval;
490+
}
491+
492+
/*
493+
* The code below uses Python internal APIs
494+
*/
495+
#include "pycore_pystate.h"
496+
497+
PyDoc_STRVAR(test_install_PEP523_eval_frame_hook__doc__,
498+
"test_install_PEP523_eval_frame_hook(*, store_args, store_result, reset=False) -- a test function.\n\
499+
This function tests the PEP-523 eval_frame-hook. Usually it is not used by Stackless Python.");
500+
501+
static PyObject* test_install_PEP523_eval_frame_hook(PyObject *self, PyObject *args, PyObject *kwds) {
502+
static char *kwlist[] = { "throw", "store_args", "reset", NULL };
503+
PyThreadState *tstate = PyThreadState_GET();
504+
PyObject *throw = NULL;
505+
PyObject *store_args = NULL;
506+
PyObject *reset = Py_False;
507+
PyObject *retval;
508+
509+
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|$O!OO!:test_install_PEP523_eval_frame_hook", kwlist,
510+
PyExc_Exception, &throw, &store_args, &PyBool_Type, &reset))
511+
return NULL;
512+
513+
if (PyObject_IsTrue(reset)) {
514+
tstate->interp->eval_frame = _PyEval_EvalFrameDefault;
515+
Py_CLEAR(pep523_frame_hook_throw);
516+
Py_CLEAR(pep523_frame_hook_store_args);
517+
} else {
518+
519+
if (throw) {
520+
Py_INCREF(throw);
521+
Py_XSETREF(pep523_frame_hook_throw, throw);
522+
}
523+
if (store_args) {
524+
Py_INCREF(store_args);
525+
Py_XSETREF(pep523_frame_hook_store_args, store_args);
526+
}
527+
528+
tstate->interp->eval_frame = pep523_frame_hook_eval_frame;
529+
}
530+
retval = pep523_frame_hook_result;
531+
pep523_frame_hook_result = NULL;
532+
if (!retval) {
533+
retval = Py_Ellipsis; /* just something different from None and NotImplemented */
534+
Py_INCREF(retval);
535+
}
536+
return retval;
537+
}
538+
453539

454540
/* ---------- */
455541

@@ -465,6 +551,8 @@ static PyMethodDef _teststackless_methods[] = {
465551
test_cstate__doc__ },
466552
{ "test_PyEval_EvalFrameEx", (PyCFunction)(void(*)(void))test_PyEval_EvalFrameEx, METH_VARARGS | METH_KEYWORDS,
467553
test_PyEval_EvalFrameEx__doc__ },
554+
{ "test_install_PEP523_eval_frame_hook", (PyCFunction)(void(*)(void))test_install_PEP523_eval_frame_hook, METH_VARARGS | METH_KEYWORDS,
555+
test_install_PEP523_eval_frame_hook__doc__ },
468556
{NULL, NULL} /* sentinel */
469557
};
470558

Stackless/unittests/test_capi.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import subprocess
3434
import glob
3535
import pickle
36+
import types
3637
import _teststackless
3738
from support import test_main # @UnusedImport
3839
from support import (StacklessTestCase, withThreads, require_one_thread,
@@ -375,6 +376,85 @@ def _test_soft_switchable_extension(self):
375376
sys.stdout.buffer.write(output)
376377

377378

379+
class Test_pep523_frame_hook(StacklessTestCase):
380+
def setUp(self):
381+
super().setUp()
382+
self.is_done = False
383+
384+
def tearDown(self):
385+
super().tearDown()
386+
_teststackless.test_install_PEP523_eval_frame_hook(reset=True)
387+
self.is_done = None
388+
389+
def record_done(self, fail=False):
390+
self.is_done = sys._getframe()
391+
if fail:
392+
4711 / 0
393+
return id(self)
394+
395+
def filter_audit_frames(self, l):
396+
# the test runner installs an audit callback. We have no control
397+
# over this callback, but we can observe its frames.
398+
return list(i for i in l if "audit" not in i[0].f_code.co_name)
399+
400+
def test_hook_delegates_to__PyEval_EvalFrameDefault(self):
401+
# test, that the hook function delegates to _PyEval_EvalFrameDefault
402+
args = []
403+
r1 = _teststackless.test_install_PEP523_eval_frame_hook(store_args=args.append)
404+
try:
405+
r2 = self.record_done()
406+
finally:
407+
r3 = _teststackless.test_install_PEP523_eval_frame_hook(reset=True)
408+
409+
self.assertIs(r1, ...)
410+
self.assertIsInstance(self.is_done, types.FrameType)
411+
self.assertEqual(r2, id(self))
412+
self.assertEqual(r3, r2)
413+
args = self.filter_audit_frames(args)
414+
self.assertListEqual(args, [(self.is_done, 0)])
415+
416+
def test_hook_delegates_to__PyEval_EvalFrameDefault_exc(self):
417+
# test, that the hook function delegates to _PyEval_EvalFrameDefault
418+
args = []
419+
r1 = _teststackless.test_install_PEP523_eval_frame_hook(store_args=args.append)
420+
try:
421+
try:
422+
self.record_done(fail=True)
423+
finally:
424+
r2 = _teststackless.test_install_PEP523_eval_frame_hook(reset=True)
425+
except ZeroDivisionError as e:
426+
self.assertEqual(str(e), "division by zero")
427+
else:
428+
self.fail("Expected exception")
429+
430+
self.assertIs(r1, ...)
431+
self.assertIsInstance(self.is_done, types.FrameType)
432+
self.assertEqual(r2, NotImplemented)
433+
args = self.filter_audit_frames(args)
434+
self.assertListEqual(args, [(self.is_done, 0)])
435+
436+
def test_hook_raises_exception(self):
437+
# test, that the hook function delegates to _PyEval_EvalFrameDefault
438+
args = []
439+
_teststackless.test_install_PEP523_eval_frame_hook(store_args=args.append,
440+
throw=ZeroDivisionError("0/0"))
441+
try:
442+
try:
443+
self.record_done()
444+
finally:
445+
r = _teststackless.test_install_PEP523_eval_frame_hook(reset=True)
446+
except ZeroDivisionError as e:
447+
self.assertEqual(str(e), "0/0")
448+
else:
449+
self.fail("Expected exception")
450+
451+
self.assertIs(self.is_done, False)
452+
self.assertEqual(r, ...)
453+
self.assertEqual(len(args), 1)
454+
self.assertIsInstance(args[0][0], types.FrameType)
455+
self.assertEqual(args[0][1], 0)
456+
457+
378458
if __name__ == "__main__":
379459
if not sys.argv[1:]:
380460
sys.argv.append('-v')

0 commit comments

Comments
 (0)