Python 调用 C 扩展与库机制全解析

以下内容有ChatGPT和Claude.ai辅助生成

系统梳理 Python C 扩展、动态库、ABI、多语言互操作及第三方库加载机制。


1️⃣ Python C 扩展基础

完整示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <Python.h>

static PyObject* py_add(PyObject* self, PyObject* args) {
int a, b;
if (!PyArg_ParseTuple(args, "ii", &a, &b)) return NULL;
return PyLong_FromLong(a + b);
}

static PyMethodDef methods[] = {
{"add", py_add, METH_VARARGS, "Add two integers"},
{NULL, NULL, 0, NULL}
};

static struct PyModuleDef module = {
PyModuleDef_HEAD_INIT, "my_module", NULL, -1, methods
};

PyMODINIT_FUNC PyInit_my_module(void) {
return PyModule_Create(&module);
}

编译(setup.py):

1
2
from setuptools import setup, Extension
setup(ext_modules=[Extension('my_module', sources=['my_module.c'])])
1
python setup.py build_ext --inplace

使用:

1
2
import my_module
result = my_module.add(3, 5) # 返回 8

执行原理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import my_module

dlopen() 加载 .so 到进程内存

查找并调用 PyInit_my_module

注册函数到 sys.modules

调用 my_module.add(3, 5)

PyArg_ParseTuple:Python object → C int

C 函数执行(CPU 直接执行机器码)

PyLong_FromLong:C int → Python object

返回结果

性能特征:

  • 动态库加载到 Python 进程内存,同进程内执行
  • 主要开销在类型转换(Python object ↔ C types)
  • C 函数本身接近原生性能,无虚拟机开销
  • 需手动管理 Python 对象引用计数(Py_INCREF/Py_DECREF
  • 默认持有 GIL,多线程场景需显式释放

2️⃣ Python 调用 C 的多种方式

方式 适用场景 优点 缺点
C API 高性能扩展、底层控制 性能最优、完全控制 代码复杂、手动管理引用
ctypes 调用现有 C 库 无需编译、纯 Python 性能较低、类型不安全
CFFI C 库绑定 代码简洁、支持 ABI/API 模式 需额外依赖
Cython Python 代码加速 语法接近 Python、渐进优化 需编译步骤
pybind11 C++ 库绑定 现代 C++、自动类型转换 仅支持 C++

ctypes 示例:

1
2
3
4
5
6
import ctypes
libc = ctypes.CDLL('libc.so.6') # Linux
strlen = libc.strlen
strlen.argtypes = [ctypes.c_char_p]
strlen.restype = ctypes.c_int
result = strlen(b"Hello") # 返回 5

3️⃣ 动态库 vs 静态库

特性 静态库 (.a/.lib) 动态库 (.so/.dll/.dylib)
链接时机 编译时 运行时
包含方式 代码拷贝进可执行文件 独立文件
内存使用 每进程独立副本 代码段多进程共享
更新方式 需重新编译链接 直接替换库文件
Python 使用 不可直接 import 可直接 import

动态库加载流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
dlopen("lib.so")

查找库文件(LD_LIBRARY_PATH、/lib、/usr/lib)

mmap 映射到内存
├─ 代码段:只读、可共享
├─ 数据段:可读写、进程独立
└─ BSS 段:未初始化数据

重定位(修正代码中的地址引用)

符号绑定(延迟绑定 BIND_LAZY / 立即绑定 BIND_NOW)

执行初始化函数(C++: 全局对象构造)

4️⃣ ABI(应用二进制接口)

定义与组成

ABI 规定二进制层面的调用规范,确保不同编译器/语言生成的代码能互操作。

核心要素:

  1. 调用约定:参数传递方式(寄存器/栈)、返回值、栈清理责任
  2. 数据布局:类型大小、结构体对齐、字节序
  3. 符号规则:C 直接导出,C++ 名称修饰(需 extern "C"
  4. 异常处理:栈展开机制

常见调用约定

约定 参数传递 清栈 平台
cdecl 栈(右→左) 调用者 Linux/Windows C 默认
System V x64 寄存器(rdi,rsi,rdx,rcx,r8,r9) - Linux x64
MS x64 寄存器(rcx,rdx,r8,r9) - Windows x64

API vs ABI

维度 API ABI
层级 源码 二进制/机器码
内容 函数声明、类型定义 调用约定、内存布局、寄存器使用
兼容性 源码兼容 二进制兼容
示例 int add(int, int) 参数通过 rdi, rsi 传递

5️⃣ 多语言动态库对比

运行时特征

语言 运行时组成 库体积 导出限制
C 无/最小 CRT 最小
C++ CRT + libstdc++ + 异常处理 + RTTI 中等 extern "C"
Rust panic 处理 + allocator #[no_mangle]
Go goroutine 调度器 + GC + 内存管理 大(数 MB) 仅基本类型和指针

代码示例

Rust:

1
2
#[no_mangle]
pub extern "C" fn rust_add(a: i32, b: i32) -> i32 { a + b }
1
cargo build --release --crate-type cdylib

Go:

1
2
3
4
import "C"
//export GoAdd
func GoAdd(a, b C.int) C.int { return a + b }
func main() {}
1
go build -buildmode=c-shared -o libgo.so

C++:

1
2
3
extern "C" {
int cpp_add(int a, int b) { return a + b; }
}

适用场景:

  • C/Rust:轻量高性能库,适合纯计算、数据处理
  • C++:复杂系统,需注意不同编译器 ABI 兼容性
  • Go:包含完整 runtime,适合独立服务,不适合作为轻量库

6️⃣ Python 第三方库机制

安装流程(以 NumPy 为例)

1
2
3
4
5
6
7
8
pip install numpy

查询 PyPI,下载 wheel 文件
numpy-1.24.0-cp311-cp311-manylinux_x86_64.whl
├─ Python 代码(__init__.py 等)
└─ 编译好的 C 扩展(.so)

解压到 site-packages/numpy/

导入与加载

1
import numpy as np

执行步骤:

1
2
3
4
5
6
7
8
9
10
11
检查 sys.modules 缓存

搜索 sys.path 查找 numpy/

执行 __init__.py(导入 C 扩展)

dlopen() 加载 _multiarray_umath.so

调用 PyInit_* 初始化函数

注册到 sys.modules(后续直接从缓存返回)

内存管理

  • Python 对象:由 GC 管理(引用计数 + 循环检测)
  • 动态库代码段:常驻内存直到进程退出
  • 卸载限制del sys.modules['numpy'] 仅删除引用,.so 不会真正卸载

7️⃣ 核心总结

  1. 执行机制:C 扩展是编译后的机器码,在 Python 进程内直接执行
  2. 性能瓶颈:主要在 Python ↔ C 类型转换,而非函数调用本身
  3. 动态库:运行时加载、代码段共享、常驻内存直到进程退出
  4. ABI:二进制接口规范,确保不同语言/编译器的代码能互操作
  5. 多语言库:Rust 轻量、Go 自带完整 runtime、C++ 需注意 ABI 兼容性
  6. 第三方库:通过 wheel 分发,包含 Python 代码和编译好的 C 扩展

参考资源: