一条 Python 语句在 C 扩展里到底怎么跑

17 阅读3分钟

要真正搞清“一条 Python 语句在 C 扩展里到底怎么跑”,最好把源码、调用栈、GIL 释放点、网络收发点都“跟”一遍。下面以
MySQLdb.connection.query("SELECT …")
为例,把从 Python 层到 C 层再到 libmysqlclient 的完整路径拆给你看。
(代码行号基于 mysqlclient-python 2.2.x,MySQL-Connector/C 8.0.x,CPython 3.11)


一、Python 层:MySQLdb/init.py

import _mysql          # 这是 C 扩展模块
…
connect = _mysql.connect
Connection = _mysql.connection

MySQLdb 只是对 _mysql 做了一层薄薄的包装,真正的类叫 _mysql.connection,所以
MySQLdb.connection.query 其实就是
_mysql.connection.query


二、C 扩展层:_mysql.c

模块初始化时把 connection 类型注册到 Python:

static PyTypeObject MyConnection_Type = {
    PyVarObject_HEAD_INIT(NULL, 0)
    .tp_name = "_mysql.connection",
    .tp_methods = _mysql_ConnectionObject_methods,
    …
};

.tp_methods 里有一项:

{"query", (PyCFunction)_mysql_ConnectionObject_query, METH_VARARGS,
 PyDoc_STR("query(sql) -> None")},

因此 Python 虚拟机在执行
LOAD_ATTR + CALL_METHOD 指令时,通过 PyType 的 method resolution 直接找到
_mysql_ConnectionObject_query


三、C 函数内部:_mysql_ConnectionObject_query

static PyObject *
_mysql_ConnectionObject_query(MyConnectionObject *self, PyObject *args)
{
    char *sql;
    int  len;
    if (!PyArg_ParseTuple(args, "s#", &sql, &len))
        return NULL;

    Py_BEGIN_ALLOW_THREADS          /* 1. 主动释放 GIL */
    int r = mysql_real_query(self->conn, sql, len);
    Py_END_ALLOW_THREADS            /* 2. 重新拿 GIL */

    if (r) {
        _mysql_Exception(self);     /* 3. 把 mysql_error() 包装成 Python 异常 */
        return NULL;
    }
    Py_RETURN_NONE;
}

关键三步:

  1. 释放 GIL → 其他 Python 线程可以跑;
  2. 调 libmysqlclient 的 mysql_real_query()
  3. 出错就把 C 的 mysql_error()/mysql_errno() 转成 _mysql.ProgrammingError/DatabaseError 等 Python 异常。

四、libmysqlclient 层:mysql_real_query()

libmysqlclient 是 C 语言官方客户端库,内部做了:

  • 把 SQL 按 MySQL 协议打成 TCP 包(net_write_command()
  • 阻塞 recv() 等待 server 返回结果包
  • 如果 SQL 是 SELECT,后续还会再调 mysql_store_result()/mysql_use_result() 读结果集。

五、结果集怎么回到 Python?

如果你在 Python 里继续写:

cursor = conn.cursor()
cursor.execute("SELECT …")

cursor.execute 会再调 _mysql.connection.query,然后:

  1. _mysql_ConnectionObject_store_result()MYSQL_RES * 抓出来;
  2. PyList_New()/PyTuple_New() 把每一行转成 Python 对象;
  3. MYSQL_FIELD 的类型映射成 Python 类型(FIELD_TYPE_LONG→PyLong,FIELD_TYPE_DATETIME→PyDateTime 等)。
    这一步在 持有 GIL 的情况下做,因为要把 C 数据喂给 Python 对象。

六、小结:一条语句的“旅程”

Python 字节码
→ LOAD_ATTR “query”
→ CALL_METHOD (进入 C)
→ PyArg_ParseTuple
→ Py_BEGIN_ALLOW_THREADS
→ libmysqlclient:mysql_real_query (阻塞等网络)
→ Py_END_ALLOW_THREADS
→ 出错则 _mysql_Exception
→ 返回 Py_None。


七、动手验证:gdb + ltrace 看现场

  1. 装 debug 符号
    apt install python3-dbg libmysqlclient-dev mysql-client-core-8.0-dbg
  2. 起 gdb
    gdb -ex r --args python3 -c "
    import MySQLdb, time
    c=MySQLdb.connect(user='root',passwd='***')
    c.query('SELECT 1')
    "
    
  3. 下断点
    (gdb) b _mysql_ConnectionObject_query
    (gdb) b mysql_real_query
  4. 单步
    (gdb) n 可以看到 Py_BEGIN_ALLOW_THREADS 宏展开后调用
    PyThread_release_lock()
    继续 s 能跟到 net_write_command()send()/recv()

八、写个最小 C 扩展 Demo(把上面流程自己跑一遍)

// demo.c
#define PY_SSIZE_T_CLEAN
#include <Python.h>
#include <mysql.h>

typedef struct {
    PyObject_HEAD
    MYSQL *mysql;
} DemoConn;

static PyObject *
DemoConn_query(DemoConn *self, PyObject *args)
{
    char *sql;
    Py_ssize_t len;
    if (!PyArg_ParseTuple(args, "s#", &sql, &len))
        return NULL;

    Py_BEGIN_ALLOW_THREADS
    int err = mysql_real_query(self->mysql, sql, len);
    Py_END_ALLOW_THREADS

    if (err) {
        PyErr_Format(PyExc_RuntimeError, "mysql: %s", mysql_error(self->mysql));
        return NULL;
    }
    Py_RETURN_NONE;
}

static PyMethodDef DemoConn_methods[] = {
    {"query", (PyCFunction)DemoConn_query, METH_VARARGS, ""},
    {NULL}
};

static PyTypeObject DemoConnType = {
    PyVarObject_HEAD_INIT(NULL, 0)
    .tp_name = "demo.Connection",
    .tp_basicsize = sizeof(DemoConn),
    .tp_flags = Py_TPFLAGS_DEFAULT,
    .tp_methods = DemoConn_methods,
};

static PyModuleDef demomodule = {
    PyModuleDef_HEAD_INIT,
    .m_name = "demo",
    .m_size -1,
};

PyMODINIT_FUNC
PyInit_demo(void)
{
    PyObject *m = PyModule_Create(&demomodule);
    if (!m) return NULL;
    if (PyType_Ready(&DemoConnType) < 0) return NULL;
    PyModule_AddObject(m, "Connection", (PyObject*)&DemoConnType);
    return m;
}

编译

python3 -m pip install mysqlclient  # 确保有头文件
gcc -shared -fPIC $(python3-config --includes) \
    -I/usr/include/mysql -L/usr/lib/x86_64-linux-gnu \
    demo.c -lmysqlclient -o demo$(python3-config --extension-suffix)

测试

python3 -c "
import demo, MySQLdb
c = demo.Connection()   # 这里省掉了 connect 参数,仅演示
c.query('SELECT 1')
"

ltrace -e mysql_real_query python3 test.py 就能抓到库函数调用。


九、随手可记的“脑图”

Python 语句
   ↓ 字节码 CALL_METHOD
C 扩展函数  (PyCFunction)
   ↓ Py_BEGIN_ALLOW_THREADS
libmysqlclient 阻塞网络
   ↓ Py_END_ALLOW_THREADS
结果 or 异常 → Python 对象