Skip to content

Commit 1d74c3b

Browse files
author
Anselm Kruis
committed
Stackless issue python#168: make Stackless compatible with old Cython modules
Many extension modules were created by Cython versions before commit 037bcf0 and were compiled with regular C-Python. These modules call PyEval_EvalFrameEx() with a broken frame object. This commit add code to recover a broken frame in PyEval_EvalFrameEx().
1 parent 4aefdd0 commit 1d74c3b

File tree

5 files changed

+85
-9
lines changed

5 files changed

+85
-9
lines changed

Objects/frameobject.c

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,13 @@ frame_dealloc(PyFrameObject *f)
447447
_PyObject_GC_UNTRACK(f);
448448

449449
Py_TRASHCAN_SAFE_BEGIN(f)
450+
451+
#if defined(STACKLESS) && PY_VERSION_HEX < 0x03080000
452+
/* Clear the magic for the old Cython frame hack.
453+
* See below in PyFrame_New() for a detailed explanation.
454+
*/
455+
f->f_blockstack[0].b_type = 0;
456+
#endif
450457
/* Kill all local variables */
451458
valuestack = f->f_valuestack;
452459
for (p = f->f_localsplus; p < valuestack; p++)
@@ -770,8 +777,28 @@ PyFrame_New(PyThreadState *tstate, PyCodeObject *code,
770777
PyObject *globals, PyObject *locals)
771778
{
772779
PyFrameObject *f = _PyFrame_New_NoTrack(tstate, code, globals, locals);
773-
if (f)
780+
if (f) {
774781
_PyObject_GC_TRACK(f);
782+
#if defined(STACKLESS) && PY_VERSION_HEX < 0x03080000
783+
if (code->co_argcount > 0) {
784+
/*
785+
* A hack for binary compatibility with Cython extension modules, which
786+
* were created with an older Cythoncompiled with regular C-Python. These
787+
* modules create frames using PyFrame_New then write to frame->f_localsplus
788+
* to set the arguments. But C-Python f_localsplus is Stackless f_code.
789+
* Therefore we add a copy of f_code and a magic number in the
790+
* uninitialized f_blockstack array.
791+
* If the blockstack is used, the magic is overwritten.
792+
* To make sure, the pointer is aligned correctly, we address it relative to
793+
* f_code.
794+
* See Stackless issue #168
795+
*/
796+
(&(f->f_code))[-1] = code;
797+
/* an arbitrary negative number which is not an opcode */
798+
f->f_blockstack[0].b_type = -31683;
799+
}
800+
#endif
801+
}
775802
return f;
776803
}
777804

Python/ceval.c

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4022,16 +4022,49 @@ _PyEval_EvalFrameDefault(PyFrameObject *f, int throwflag)
40224022
if (f == NULL)
40234023
return NULL;
40244024

4025+
/* The layout of PyFrameObject differs between Stackless and C-Python.
4026+
* Stackless f->f_execute is C-Python f->f_code. Stackless f->f_code is at
4027+
* the end, just before f_localsplus.
4028+
*/
40254029
if (PyFrame_Check(f) && f->f_execute == NULL) {
40264030
/* A new frame returned from PyFrame_New() has f->f_execute == NULL.
4031+
* Set the usual execution function.
40274032
*/
40284033
f->f_execute = PyEval_EvalFrameEx_slp;
4034+
4035+
#if PY_VERSION_HEX < 0x03080000
4036+
/* Older versions of Cython used to create frames using C-Python layout
4037+
* of PyFrameObject. As a consequence f_code is overwritten by the first
4038+
* item of f_localsplus[]. To be able to fix it, we have a copy of
4039+
* f_code and a signature at the end of the block-stack.
4040+
* The Py_BUILD_ASSERT_EXPR checks,that our assumptions about the layout
4041+
* of PyFrameObject are true.
4042+
* See Stackless issue #168
4043+
*/
4044+
(void) Py_BUILD_ASSERT_EXPR(offsetof(PyFrameObject, f_code) ==
4045+
offsetof(PyFrameObject, f_localsplus) - Py_MEMBER_SIZE(PyFrameObject, f_localsplus[0]));
4046+
4047+
/* Check for an old Cython frame */
4048+
if (f->f_iblock == 0 && f->f_lasti == -1 && /* blockstack is empty */
4049+
f->f_blockstack[0].b_type == -31683 && /* magic is present */
4050+
/* and f_code has been overwritten */
4051+
f->f_code != (&(f->f_code))[-1] &&
4052+
/* and (&(f->f_code))[-1] looks like a valid code object */
4053+
(&(f->f_code))[-1] && PyCode_Check((&(f->f_code))[-1]) &&
4054+
/* and there are arguments */
4055+
(&(f->f_code))[-1]->co_argcount > 0)
4056+
{
4057+
PyCodeObject * code = (&(f->f_code))[-1];
4058+
memmove(f->f_localsplus, f->f_localsplus-1, code->co_argcount * sizeof(f->f_localsplus[0]));
4059+
f->f_code = code;
4060+
} else
4061+
#endif
4062+
if (!(f->f_code != NULL && PyCode_Check(f->f_code))) {
4063+
PyErr_BadInternalCall();
4064+
return NULL;
4065+
}
40294066
} else {
4030-
/* The layout of PyFrameObject differs between Stackless and C-Python.
4031-
* Stackless f->f_execute is C-Python f->f_code. Stackless f->f_code is at
4032-
* the end, just before f_localsplus.
4033-
*
4034-
* In order to detect a C-Python frame, we must compare f->f_execute
4067+
/* In order to detect a broken C-Python frame, we must compare f->f_execute
40354068
* with every valid frame function. Hard to implement completely.
40364069
* Therefore I'll check only for relevant functions.
40374070
* Amend the list as needed.

Stackless/changelog.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ What's New in Stackless 3.X.X?
1313
Fix C-API functions PyEval_EvalFrameEx() and PyEval_EvalFrame().
1414
They are now compatible with C-Python.
1515

16+
- https://github.com/stackless-dev/stackless/issues/168
17+
Make Stackless compatible with old Cython extension modules compiled
18+
for regular C-Python.
19+
1620
- https://github.com/stackless-dev/stackless/issues/167
1721
Replace 'printf(...)' calls by PySys_WriteStderr(...). They are used to emit
1822
an error message, if there is a pending error while entering Stackless

Stackless/module/stacklessmodule.c

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1131,20 +1131,21 @@ by Stackless Python.\n\
11311131
The function creates a frame from code, globals and args and executes the frame.");
11321132

11331133
static PyObject* test_PyEval_EvalFrameEx(PyObject *self, PyObject *args, PyObject *kwds) {
1134-
static char *kwlist[] = {"code", "globals", "args", "alloca", "throw", NULL};
1134+
static char *kwlist[] = {"code", "globals", "args", "alloca", "throw", "oldcython", NULL};
11351135
PyThreadState *tstate = PyThreadState_GET();
11361136
PyCodeObject *co;
11371137
PyObject *globals, *co_args = NULL;
11381138
Py_ssize_t alloca_size = 0;
11391139
PyObject *exc = NULL;
1140+
PyObject *oldcython = NULL;
11401141
PyFrameObject *f;
11411142
PyObject *result = NULL;
11421143
void *p;
11431144
Py_ssize_t na;
11441145

1145-
if (!PyArg_ParseTupleAndKeywords(args, kwds, "O!O!|O!nO:test_PyEval_EvalFrameEx", kwlist,
1146+
if (!PyArg_ParseTupleAndKeywords(args, kwds, "O!O!|O!nOO!:test_PyEval_EvalFrameEx", kwlist,
11461147
&PyCode_Type, &co, &PyDict_Type, &globals, &PyTuple_Type, &co_args, &alloca_size,
1147-
&exc))
1148+
&exc, &PyBool_Type, &oldcython))
11481149
return NULL;
11491150
if (exc && !PyExceptionInstance_Check(exc)) {
11501151
PyErr_SetString(PyExc_TypeError, "exc must be an exception instance");
@@ -1175,6 +1176,13 @@ static PyObject* test_PyEval_EvalFrameEx(PyObject *self, PyObject *args, PyObjec
11751176
goto exit;
11761177
}
11771178
fastlocals = f->f_localsplus;
1179+
if (oldcython == Py_True) {
1180+
/* Use the f_localsplus offset from regular C-Python. Old versions of cython used to
1181+
* access f_localplus directly. Current versions compute the field offset for
1182+
* f_localsplus at run-time.
1183+
*/
1184+
fastlocals--;
1185+
}
11781186
for (i = 0; i < na; i++) {
11791187
PyObject *arg = PyTuple_GetItem(co_args, i);
11801188
if (arg == NULL) {

Stackless/unittests/test_capi.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@ def test_cstack_spilling(self):
7575
# Force stack spilling. 16384 is the value of CSTACK_WATERMARK from slp_platformselect.h
7676
self.call_PyEval_EvalFrameEx(None, alloca=16384 * 8)
7777

78+
def test_oldcython_frame(self):
79+
# A test for Stackless issue #168
80+
self.assertEqual(self.call_PyEval_EvalFrameEx(47110816, oldcython=True), 47110816)
81+
7882
def test_stack_unwinding(self):
7983
# Calling the __init__ method of a new-style class involves stack unwinding
8084
class C(object):

0 commit comments

Comments
 (0)