大师兄的Python源码学习笔记(十七): 虚拟机中的控制流(四)
大师兄的Python源码学习笔记(十九): 虚拟机中的函数机制(一)
四、虚拟机中的异常控制流
2 异常控制语义结构
- 首先创建一段异常捕获代码:
demo.py
try:
raise Exception("an exception")
except Exception as e:
...
finally:
...
- 对应的指令字节码如下:
1 0 SETUP_FINALLY 52 (to 54)
2 SETUP_EXCEPT 12 (to 16)
2 4 LOAD_NAME 0 (Exception)
6 LOAD_CONST 0 ('an exception')
8 CALL_FUNCTION 1
10 RAISE_VARARGS 1
12 POP_BLOCK
14 JUMP_FORWARD 34 (to 50)
3 >> 16 DUP_TOP
18 LOAD_NAME 0 (Exception)
20 COMPARE_OP 10 (exception match)
22 POP_JUMP_IF_FALSE 48
24 POP_TOP
26 STORE_NAME 1 (e)
28 POP_TOP
30 SETUP_FINALLY 4 (to 36)
4 32 POP_BLOCK
34 LOAD_CONST 1 (None)
>> 36 LOAD_CONST 1 (None)
38 STORE_NAME 1 (e)
40 DELETE_NAME 1 (e)
42 END_FINALLY
44 POP_EXCEPT
46 JUMP_FORWARD 2 (to 50)
>> 48 END_FINALLY
>> 50 POP_BLOCK
52 LOAD_CONST 1 (None)
6 >> 54 END_FINALLY
56 LOAD_CONST 1 (None)
58 RETURN_VALUE
- 开始的两个字节码SETUP_FINALLY和SETUP_EXCEPT都是调用了PyFrame_BlockSetup:
ceval.c
TARGET(SETUP_LOOP)
TARGET(SETUP_EXCEPT)
TARGET(SETUP_FINALLY) {
/* NOTE: If you add any new block-setup opcodes that
are not try/except/finally handlers, you may need
to update the PyGen_NeedsFinalizing() function.
*/
PyFrame_BlockSetup(f, opcode, INSTR_OFFSET() + oparg,
STACK_LEVEL());
DISPATCH();
}
Objects\frameobject.c
void
PyFrame_BlockSetup(PyFrameObject *f, int type, int handler, int level)
{
PyTryBlock *b;
if (f->f_iblock >= CO_MAXBLOCKS)
Py_FatalError("XXX block stack overflow");
b = &f->f_blockstack[f->f_iblock++];
b->b_type = type;
b->b_level = level;
b->b_handler = handler;
}
- 从上面的代码可以看出PyFrame_BlockSetup对当前PyFrameObject进行了配置:
Objects\frameobject.c typedef struct _frame { ... ... PyTryBlock f_blockstack[CO_MAXBLOCKS]; /* for try and loop blocks */ ... ... } PyFrameObject;
Objects\frameobject.c typedef struct { int b_type; /* what kind of block this is */ int b_handler; /* where to jump to find handler */ int b_level; /* value stack level to pop to */ } PyTryBlock;
- 接着,通过LOAD_NAME、LOAD_CONST和CALL_FUNCTION构造了一个异常对象压入栈中,并通过RAISE_VARARGS将异常对象从运行时栈中取出:
ceval.c
TARGET(RAISE_VARARGS) {
PyObject *cause = NULL, *exc = NULL;
switch (oparg) {
case 2:
cause = POP(); /* cause */
/* fall through */
case 1:
exc = POP(); /* exc */
/* fall through */
case 0:
if (do_raise(exc, cause)) {
why = WHY_EXCEPTION;
goto fast_block_end;
}
break;
default:
PyErr_SetString(PyExc_SystemError,
"bad RAISE_VARARGS oparg");
break;
}
goto error;
}
- 这里oparg参数为1,所以将异常对象取出赋值给w,接着运行do_raise函数:
ceval.c static int do_raise(PyObject *exc, PyObject *cause) { PyObject *type = NULL, *value = NULL; if (exc == NULL) { /* Reraise */ PyThreadState *tstate = PyThreadState_GET(); _PyErr_StackItem *exc_info = _PyErr_GetTopmostException(tstate); PyObject *tb; type = exc_info->exc_type; value = exc_info->exc_value; tb = exc_info->exc_traceback; if (type == Py_None || type == NULL) { PyErr_SetString(PyExc_RuntimeError, "No active exception to reraise"); return 0; } Py_XINCREF(type); Py_XINCREF(value); Py_XINCREF(tb); PyErr_Restore(type, value, tb); return 1; } /* We support the following forms of raise: raise raise <instance> raise <type> */ if (PyExceptionClass_Check(exc)) { type = exc; value = _PyObject_CallNoArg(exc); if (value == NULL) goto raise_error; if (!PyExceptionInstance_Check(value)) { PyErr_Format(PyExc_TypeError, "calling %R should have returned an instance of " "BaseException, not %R", type, Py_TYPE(value)); goto raise_error; } } else if (PyExceptionInstance_Check(exc)) { value = exc; type = PyExceptionInstance_Class(exc); Py_INCREF(type); } else { /* Not something you can raise. You get an exception anyway, just not what you specified :-) */ Py_DECREF(exc); PyErr_SetString(PyExc_TypeError, "exceptions must derive from BaseException"); goto raise_error; } assert(type != NULL); assert(value != NULL); if (cause) { PyObject *fixed_cause; if (PyExceptionClass_Check(cause)) { fixed_cause = _PyObject_CallNoArg(cause); if (fixed_cause == NULL) goto raise_error; Py_DECREF(cause); } else if (PyExceptionInstance_Check(cause)) { fixed_cause = cause; } else if (cause == Py_None) { Py_DECREF(cause); fixed_cause = NULL; } else { PyErr_SetString(PyExc_TypeError, "exception causes must derive from " "BaseException"); goto raise_error; } PyException_SetCause(value, fixed_cause); } PyErr_SetObject(type, value); /* PyErr_SetObject incref's its arguments */ Py_DECREF(value); Py_DECREF(type); return 0; raise_error: Py_XDECREF(value); Py_XDECREF(type); Py_XDECREF(cause); return 0; }
- do_raise将异常对象存储到当前线程状态中,并在结束后跳到fast_block_end。
ceval.c fast_block_end: assert(why != WHY_NOT); /* Unwind stacks if a (pseudo) exception occurred */ while (why != WHY_NOT && f->f_iblock > 0) { /* Peek at the current block. */ PyTryBlock *b = &f->f_blockstack[f->f_iblock - 1]; assert(why != WHY_YIELD); if (b->b_type == SETUP_LOOP && why == WHY_CONTINUE) { why = WHY_NOT; JUMPTO(PyLong_AS_LONG(retval)); Py_DECREF(retval); break; } /* Now we have to pop the block. */ f->f_iblock--; if (b->b_type == EXCEPT_HANDLER) { UNWIND_EXCEPT_HANDLER(b); continue; } UNWIND_BLOCK(b); if (b->b_type == SETUP_LOOP && why == WHY_BREAK) { why = WHY_NOT; JUMPTO(b->b_handler); break; } if (why == WHY_EXCEPTION && (b->b_type == SETUP_EXCEPT || b->b_type == SETUP_FINALLY)) { PyObject *exc, *val, *tb; int handler = b->b_handler; _PyErr_StackItem *exc_info = tstate->exc_info; /* Beware, this invalidates all b->b_* fields */ PyFrame_BlockSetup(f, EXCEPT_HANDLER, -1, STACK_LEVEL()); PUSH(exc_info->exc_traceback); PUSH(exc_info->exc_value); if (exc_info->exc_type != NULL) { PUSH(exc_info->exc_type); } else { Py_INCREF(Py_None); PUSH(Py_None); } PyErr_Fetch(&exc, &val, &tb); /* Make the raw exception data available to the handler, so a program can emulate the Python main loop. */ PyErr_NormalizeException( &exc, &val, &tb); if (tb != NULL) PyException_SetTraceback(val, tb); else PyException_SetTraceback(val, Py_None); Py_INCREF(exc); exc_info->exc_type = exc; Py_INCREF(val); exc_info->exc_value = val; exc_info->exc_traceback = tb; if (tb == NULL) tb = Py_None; Py_INCREF(tb); PUSH(tb); PUSH(val); PUSH(exc); why = WHY_NOT; JUMPTO(handler); break; } if (b->b_type == SETUP_FINALLY) { if (why & (WHY_RETURN | WHY_CONTINUE)) PUSH(retval); PUSH(PyLong_FromLong((long)why)); why = WHY_NOT; JUMPTO(b->b_handler); break; } } /* unwind stack */ /* End the loop if we still have an error (or return) */ if (why != WHY_NOT) break; assert(!PyErr_Occurred()); } /* main loop */ assert(why != WHY_YIELD); /* Pop remaining stack entries. */ while (!EMPTY()) { PyObject *o = POP(); Py_XDECREF(o); } if (why != WHY_RETURN) retval = NULL; assert((retval != NULL) ^ (PyErr_Occurred() != NULL));
- 在这里,虚拟机首先从当前PyFrameObject对象中的f_blockstack弹出一个PyTryBlock来。
- 接着虚拟机通过PyErr_Fetch获取当前线程状态对象中存储的最新异常对象和traceback对象。
ceval.c void PyErr_Fetch(PyObject **p_type, PyObject **p_value, PyObject **p_traceback) { PyThreadState *tstate = PyThreadState_GET(); *p_type = tstate->curexc_type; *p_value = tstate->curexc_value; *p_traceback = tstate->curexc_traceback; tstate->curexc_type = NULL; tstate->curexc_value = NULL; tstate->curexc_traceback = NULL; }
- 随后,虚拟机将tb、val和exc压入到运行时栈中,并将why设置为WHY_NOT。
- 最后通过JUMPTO(b->b_handler)将要执行的吓一跳指令设置为异常处理代码编译后所得到的第一条字节码指令。
- 经过偏移后,字节码指令跳转字节码DUP_TOP的位置,它是异常处理代码队形的第一条字节码指令:
ceval.c
TARGET(DUP_TOP) {
PyObject *top = TOP();
Py_INCREF(top);
PUSH(top);
FAST_DISPATCH();
}
- 在异常处理中,首先通过COMPARE_OP进行比对,用来判断是否需要使用POP_JUMP_IF_FALSE进行指令跳跃:
ceval.c
TARGET(COMPARE_OP) {
PyObject *right = POP();
PyObject *left = TOP();
PyObject *res = cmp_outcome(oparg, left, right);
Py_DECREF(left);
Py_DECREF(right);
SET_TOP(res);
if (res == NULL)
goto error;
PREDICT(POP_JUMP_IF_FALSE);
PREDICT(POP_JUMP_IF_TRUE);
DISPATCH();
}
- 由于在进入except之前,虚拟机已经从当前线程将异常信息tb、val和exc提取了出来。
- 当COMPARE_OP匹配时,异常信息会从栈中取出来处理掉。
- 而当COMPARE_OP不匹配时,则需要将异常信息重新放回到线程状态对象中,然后重新设置why状态,寻找真正能处理异常的代码,通过字节指令码POP TOP和SETUP_FINALLY完成:
ceval.c
if (opcode == SETUP_FINALLY ||
opcode == SETUP_WITH ||
opcode == BEFORE_ASYNC_WITH ||
opcode == YIELD_FROM) {
/* Few cases where we skip running signal handlers and other
pending calls:
- If we're about to enter the 'with:'. It will prevent
emitting a resource warning in the common idiom
'with open(path) as file:'.
- If we're about to enter the 'async with:'.
- If we're about to enter the 'try:' of a try/finally (not
*very* useful, but might help in some cases and it's
traditional)
- If we're resuming a chain of nested 'yield from' or
'await' calls, then each frame is parked with YIELD_FROM
as its next opcode. If the user hit control-C we want to
wait until we've reached the innermost frame before
running the signal handler and raising KeyboardInterrupt
(see bpo-30039).
*/
goto fast_next_opcode;
}
if (_Py_atomic_load_relaxed(
&_PyRuntime.ceval.pending.calls_to_do))
{
if (Py_MakePendingCalls() < 0)
goto error;
}
if (_Py_atomic_load_relaxed(
&_PyRuntime.ceval.gil_drop_request))
{
/* Give another thread a chance */
if (PyThreadState_Swap(NULL) != tstate)
Py_FatalError("ceval: tstate mix-up");
drop_gil(tstate);
/* Other threads may run now */
take_gil(tstate);
/* Check if we should make a quick exit. */
if (_Py_IsFinalizing() &&
!_Py_CURRENTLY_FINALIZING(tstate))
{
drop_gil(tstate);
PyThread_exit_thread();
}
if (PyThreadState_Swap(tstate) != NULL)
Py_FatalError("ceval: orphan tstate");
}
/* Check for asynchronous exceptions. */
if (tstate->async_exc != NULL) {
PyObject *exc = tstate->async_exc;
tstate->async_exc = NULL;
UNSIGNAL_ASYNC_EXC();
PyErr_SetNone(exc);
Py_DECREF(exc);
goto error;
}
}
- 从上面的代码的结尾处,重新跳转到error设置了why。
- 最终不管是否匹配,两条岔路在POP_BLOCK会和:
ceval.c
PREDICTED(POP_BLOCK);
TARGET(POP_BLOCK) {
PyTryBlock *b = PyFrame_BlockPop(f);
UNWIND_BLOCK(b);
DISPATCH();
}
- 这里将PyTryBlock弹出,并进入finally表达式对应的字节码指令。
-
最终Python实现异常机制的详细流程如下:
网友评论