一条 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 对象
相关推荐
夜雪一千3 分钟前
Python URL解码详解:单层、双重URL解码实战与踩坑指南
linux·服务器·python
VIP_CQCRE5 分钟前
用 Ace Data Cloud 一站式接入 AI 视频生成:HappyHorse Videos API 实战指南
python·aigc·api·ai视频·acedatacloud
kali-Myon9 分钟前
定向 FGSM 攻击:让神经网络《指猫为狗》
图像处理·python·神经网络·安全·机器学习·fgsm
老歌老听老掉牙12 分钟前
旅行商问题:从数学模型到 Python 实战
开发语言·python
CC数分12 分钟前
数据可视化入门:用Python把一堆数字变成一张图
开发语言·python·信息可视化·数据分析
打工仔折腾 AI20 分钟前
Prometheus接入Pushgateway实战:二进制与Docker部署、指标推送与远程写入
后端·python·docker·容器·性能优化·prometheus·ai agent 实战
FYKJ_20101 小时前
springboot鲜花销售系统91056-计算机课程设计、毕业设计
vue.js·spring boot·后端·python·mysql·django·课程设计
子非鱼eva1 小时前
ONNX模型导出实战:PyTorch 导出 ResNet18 模型
人工智能·pytorch·python·知识图谱·onnx·昇腾知识图谱
Andreapiki1 小时前
数字化审计校招技术备考指南:SQL、Python、BI、RPA学习路径
python·sql·rpa
Rebecca_aLi1 小时前
码匠教育:零基础学 Python,标识符到循环全套基础语法解析
开发语言·python·正则表达式