C语言调用Python:Python C API嵌入基础与步骤

方予
方予
发布时间:2026-10-21 | 3人浏览过
原创

在高性能计算、游戏引擎或工业控制软件等以 C/C++ 为主的底层工程中,将 Python 运行时直接嵌入到 C 程序中,是兼顾底层执行速度与上层动态扩展能力的经典方案。通过官方提供的 Python C API,C 语言不仅能作为宿主环境启动并托管完整的 Python 解释器,还能动态加载 .py 脚本、传递参数调用目标函数,并获取处理结果。本文将详细讲解 Python C API 的底层嵌入原理、编译链接环境配置、核心调用生命周期,以及不可忽视的引用计数内存安全规则。

一、嵌入架构与进程内存模型

在传统的 Python 脚本运行模式中,操作系统的控制权属于 python3 解释器进程;而在嵌入式模式(Embedding)下,进程的入口点是 C 程序的 main() 函数。C 语言程序直接链接官方动态库 libpython,并在自身进程地址空间内初始化一个轻量级的 Python 解释器实例。

C语言调用Python:Python C API嵌入基础与步骤
图 1:C 语言宿主进程与 Python C API 嵌入式运行时架构

在这个架构中,所有的交互都基于一个核心数据类型:PyObject*。无论是整数、字符串、自定义类实例,还是模块与函数本身,在 C 语言的视角下全都是指向 PyObject 结构体的指针。C 代码与 Python 解释器打交道的过程,实质上就是不断构建、传递、解包并释放 PyObject* 的过程。

二、开发环境与编译链接配置

在编写 C 代码前,必须安装包含头文件与静态/动态库的 Python 开发包。在主流 Linux 发行版或 macOS 上,这一步骤必不可少:

  • Debian / Ubuntu: sudo apt install python3-dev
  • RHEL / CentOS / Fedora: sudo dnf install python3-devel
  • macOS (Homebrew): 通常随 brew install python@3.x 自带完整开发头文件

由于不同操作系统和 Python 版本的头文件安装路径、动态库名称差异较大,硬编码编译器参数极其容易导致链接失败。官方提供了 python3-config 辅助脚本,用于动态输出当前环境下所需的预处理器包含路径与链接标志。

C语言调用Python:Python C API嵌入基础与步骤
图 2:C 语言嵌入 Python 的编译链接流向图

三、C 语言调用 Python 的关键步骤与代码实现

一次标准的 Python 函数调用,在 C API 的维度需要经过严格的六个顺序步骤:初始化运行时、加载模块、提取函数、构建调用参数、执行调用并解包、释放引用与终结解释器。

C语言调用Python:Python C API嵌入基础与步骤
图 3:Python C API 嵌入调用的标准六步生命周期模型

完整代码示例

以下是一个可以完整编译运行的示例。假设我们需要在当前目录下调用一个 Python 计算脚本 calc.py 中的加法函数 add(a, b)。

首先编写 Python 脚本 calc.py:

# calc.py
def add(a: int, b: int) -> int:
    """计算两数相加并打印日志"""
    print(f"[Python] 收到 C 传递的参数: a = {a}, b = {b}")
    return a + b

接着编写 C 语言主程序 main.c:

#define PY_SSIZE_T_CLEAN
#include <Python.h>
#include <stdio.h>

int main(void) {
    // 1. 初始化 Python 解释器
    Py_Initialize();
    if (!Py_IsInitialized()) {
        fprintf(stderr, "错误: Python 解释器初始化失败\n");
        return 1;
    }

    // 将当前工作目录 "." 动态加入 sys.path,保证解释器能定位到同一目录下的 calc.py
    PyRun_SimpleString("import sys; sys.path.append('.')");

    // 2. 导入模块 calc.py
    PyObject *pModuleName = PyUnicode_DecodeFSDefault("calc");
    PyObject *pModule = PyImport_Import(pModuleName);
    Py_DECREF(pModuleName); // 及时释放模块名字符串对象

    if (pModule == NULL) {
        fprintf(stderr, "错误: 无法导入 calc.py 模块\n");
        PyErr_Print(); // 打印 Python 回溯栈
        Py_Finalize();
        return 1;
    }

    // 3. 从模块中提取目标函数 add
    PyObject *pFunc = PyObject_GetAttrString(pModule, "add");
    if (pFunc == NULL || !PyCallable_Check(pFunc)) {
        fprintf(stderr, "错误: 未找到可调用的 add 函数\n");
        if (PyErr_Occurred()) {
            PyErr_Print();
        }
        Py_XDECREF(pFunc);
        Py_DECREF(pModule);
        Py_Finalize();
        return 1;
    }

    // 4. 构建参数元组 (15, 27)
    PyObject *pArgs = Py_BuildValue("(ii)", 15, 27);
    if (pArgs == NULL) {
        fprintf(stderr, "错误: 构建参数元组失败\n");
        Py_DECREF(pFunc);
        Py_DECREF(pModule);
        Py_Finalize();
        return 1;
    }

    // 5. 执行函数调用并解包结果
    PyObject *pValue = PyObject_CallObject(pFunc, pArgs);
    Py_DECREF(pArgs); // 实参已传入,可释放参数元组

    if (pValue != NULL) {
        long result = PyLong_AsLong(pValue);
        printf("[C 程序] Python 返回值解析成功: result = %ld\n", result);
        Py_DECREF(pValue); // 释放返回值对象
    } else {
        fprintf(stderr, "错误: 函数调用执行失败\n");
        PyErr_Print();
    }

    // 6. 释放函数与模块引用,关闭解释器
    Py_DECREF(pFunc);
    Py_DECREF(pModule);
    Py_Finalize();

    return 0;
}
编译与执行指令
# 单行完成编译与链接
gcc main.c -o run_py $(python3-config --cflags) $(python3-config --ldflags --embed)

# 运行程序
./run_py

输出效果:

[Python] 收到 C 传递的参数: a = 15, b = 27
[C 程序] Python 返回值解析成功: result = 42

四、内存管理与引用计数规则

在 C 语言调用 Python API 的所有环节中,引用计数(Reference Counting)是最容易引发内存泄漏或严重段错误(Segmentation Fault)的核心所在。Python 对象通过维护内部的引用计数器决定何时回收内存。

开发者必须明确区分两种不同的引用获取方式:

  • 新引用 (New Reference):API 函数返回一个引用计数已经加 1 的全新或现有对象。调用方成为该引用的拥有者(Owner),在用完后必须显式调用 Py_DECREF() 或 Py_XDECREF(),否则该对象将永久驻留内存。典型函数包括 PyImport_Import()、PyObject_GetAttrString()、Py_BuildValue()、PyObject_CallObject()。
  • 借用引用 (Borrowed Reference):API 函数仅返回一个借出给调用者使用的指针,并未增加引用计数。调用者绝不能对其调用 Py_DECREF(),否则会导致计数提前归零引发野指针释放。典型函数包括 PyTuple_GetItem()、PyList_GetItem()。如果需要长期持有借用来的对象,必须先调用 Py_INCREF() 手动认领所有权。

五、知识体系总结与常见问题排查

为了帮助读者在遇到运行异常时快速排查,下表与决策图汇总了 Python C API 嵌入中最常见的故障现象、根本成因与应对措施:

C语言调用Python:Python C API嵌入基础与步骤
图 4:Python C API 引用计数管理准则与排查决策矩阵

常见问题与排查清单

问题场景 典型错误提示 排查方向与解决手段
模块导入失败 PyImport_Import 返回 NULL 检查 sys.path 中是否包含脚本所在路径。Python 嵌入解释器默认不加入当前目录,需显式 sys.path.append('.')。
函数提取失败 PyObject_GetAttrString 返回 NULL 检查函数名称拼写是否一致,并使用 PyCallable_Check() 确认该属性是否为可调用对象。
链接时符号未定义 undefined reference to 'Py_Initialize' 确保在编译命令中追加了 $(python3-config --ldflags --embed),且参数放在 main.c 之后。
程序偶发段错误 Segmentation fault (core dumped) 检查是否存在多次 Py_DECREF 同一指针的情况,或对 NULL 执行了 Py_DECREF。改用 Py_XDECREF 并置空指针。
多线程死锁或崩溃 Fatal Python error: take_gil: NULL tstate 在非主线程调用 API 时未持有全局解释器锁。必须使用 PyGILState_Ensure() 获取锁,并在处理完成后 PyGILState_Release() 归还。

通过遵循上述的六步生命周期、规范的构建参数以及严格的引用计数控制,开发者可以在 C 语言程序中稳定、安全地嵌入 Python 解释器,充分发挥 C 底层性能与 Python 生态灵活性相结合的强大优势。

相关标签:

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表