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

要真正搞清"一条 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

python 复制代码
import _mysql          # 这是 C 扩展模块
...
connect = _mysql.connect
Connection = _mysql.connection

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


二、C 扩展层:_mysql.c

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

c 复制代码
static PyTypeObject MyConnection_Type = {
    PyVarObject_HEAD_INIT(NULL, 0)
    .tp_name = "_mysql.connection",
    .tp_methods = _mysql_ConnectionObject_methods,
    ...
};

.tp_methods 里有一项:

c 复制代码
{"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

c 复制代码
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 里继续写:

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

    sql 复制代码
    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(把上面流程自己跑一遍)

c 复制代码
// 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;
}

编译

makefile 复制代码
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)

测试

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

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


九、随手可记的"脑图"

objectivec 复制代码
Python 语句
   ↓ 字节码 CALL_METHOD
C 扩展函数  (PyCFunction)
   ↓ Py_BEGIN_ALLOW_THREADS
libmysqlclient 阻塞网络
   ↓ Py_END_ALLOW_THREADS
结果 or 异常 → Python 对象
相关推荐
北斗落凡尘14 小时前
LangGraph 入门实战(11)--输出模式
后端·python·langchain
雪碧聊技术15 小时前
安装Python(保姆级教程)
python·版本更新·python下载
张龙68717 小时前
uv 全面指南:比 pip 快 100 倍的 Python 包管理器,从安装到团队落地
后端·python
@HNUSTer18 小时前
Python数据可视化科技图表绘制系列教程(八)
python·数据可视化·科技论文·专业制图·科研图表
专注仿真18 小时前
问答大模型技术方案算法实现-熵权法融合算法 + 交叉编码器重排算法
人工智能·python·算法·语言模型·问答大模型关键算法·熵权融合算法·交叉编码器重排算法
满怀冰雪19 小时前
22-使用 PaddleClas 快速训练图像分类模型
人工智能·python·机器学习·分类·数据挖掘·paddlepaddle
gb421528719 小时前
python中Web应用服务器
开发语言·前端·python
reasonsummer21 小时前
【办公类-109-12】20260901圆形挂牌(模版重制:文本框圆形重叠_word编辑单面_接送卡&被子卡&床卡&入园卡)
python·word
名明鸣冥21 小时前
k8s-agent架构思考(一)
python
DoLovya21 小时前
从零实现 ESP8266 + MQTT 智能门禁:公网一键开门、舵机控制、在线监控完整方案
python·物联网·mqtt·嵌入式·esp8266·门禁系统