Skip to content

构建第一个扩展模块

本章将带你一步步完成一个最简单的 Python C 扩展模块的构建过程,
包括 CMake 配置和绑定一个基础函数,实现 Python 调用 C 函数。

环境准备

  • 已安装 Python(建议使用 Python 3.10+)
  • 已安装 CMake(对于VS2022可直接使用内置的集成版本)
  • C/C++ 编译器(Windows 下推荐 MSVC,Linux/macOS 推荐 GCC/Clang)

若无特殊声明,文案内容默认以MSVC(VS2022)+CAMKE为标准。

编写 CMakeLists.txt

使用 CMake 配置并管理此 C++ 项目。

text
TestLib/
├── CMakeLists.txt
└── main.cpp
cmake
# CMakeLists.txt
cmake_minimum_required (VERSION 3.11)

project(PYTHON_TEST)

if(MSVC)
    # 为MSVC设置UTF8编码支持以及C++异常处理支持(可选)
    add_compile_options(/utf-8 /EHsc)
endif()

# 包含Python头文件和库目录 Windows可通过CMD: 'where python' 快速查询Python路径
include_directories("D:/Python/Python312/include")
link_directories("D:/Python/Python312/libs")

# 创建一个名为testLib的动态链接库编译目标 其中包含main.cpp源代码文件
add_library(testLib SHARED "main.cpp")

if(WIN32)
    # 对于windows平台 需要设置库文件后缀为.pyd而不是.dll(以便python识别)
    set_target_properties(testLib PROPERTIES SUFFIX ".pyd")
endif()

# 链接python312.lib库文件(取决于你的python版本)
target_link_libraries(testLib PRIVATE python312.lib)

# 设置C++语言标准为20(可选)
set_property(TARGET testLib PROPERTY CXX_STANDARD 20)

编写模块入口

Python解释器会尝试加载特定符号以便获取模块注册信息。

cpp
// main.cpp
#include <iostream>
#include <Python.h>  // 包含Python所需的一系列C-API以及宏定义

// 模块导出函数表
static PyMethodDef moduleMethods[] = {
    {NULL, NULL, 0, NULL},  // 末尾标识,用于VM内存搜索的终止符
};

// 入口符号命名规范: PyInit_{lib文件名}
PyMODINIT_FUNC PyInit_testLib()
{
    static PyModuleDef moduleDef = {
        PyModuleDef_HEAD_INIT,
        "testLib",  // 定义module的__name__信息
        "",         // 定义module的__doc__信息
        -1,         // 不支持多vm模式(即全局模块共享)
        moduleMethods,       // 模块导出函数表定义
        NULL, NULL, NULL, NULL  // 其余不常用参数 可忽略/填充默认值NULL
    };
    // PyModule_Create 创建Module并返回给PyVM
    return PyModule_Create(&moduleDef);
}

第一个C函数绑定

实现一个简单的函数绑定。

cpp
// 对于函数而言,PyObject* self代表当前模块对象
static PyObject* pyAdd(PyObject* self, PyObject* args)
{
    long long a, b;
    // 解析Python函数传参(所有的参数会打包成元组) 并把结果写入特定内存中(&a, &b)
    if (!PyArg_ParseTuple(args, "ll", &a, &b))
    {
        // 参数解析失败,如不符合ll类型(即两个long long)返回NULL指针代表异常信号
        return NULL;
    }
    long long result = a + b;
    // 完成计算 构造Py Long对象(int)
    return PyLong_FromLong(result);
}

// 添加绑定函数信息
static PyMethodDef moduleMethods[] = {
    // {导出函数名, 绑定函数指针, 调用约定, __doc__}
    // METH_VARARGS 调用约定, 表示函数接受参数元组 (args),对于无参函数有其他声明
    // 常用约定:METH_NOARGS,METH_VARARGS,METH_KEYWORDS(可通过位运算|同时使用多个约定)
    {"add", pyAdd, METH_VARARGS, "函数 __doc__ 可忽略"},
    {NULL, NULL, 0, NULL},
};

以上代码编译后将生成的.pyd文件放置在python项目内/配置到解释器环境库中即可import使用。

  • 代码测试
python
import testLib  # 确保可以搜索到目标pyd文件

print(testLib.add(10000, 2000)) # -> 正常输出计算结果

编写pyi文件

扩展模块为二进制分发,编辑器/IDE无法正常完成分析。

python
# testLib.pyi (放置在testLib.pyd同级)
# 通过pyi文件提供声明信息,可辅助开发工具完成分析高亮补全。
def add(a: int, b: int) -> int: ...

CPP-STD类型交互

局部混用C++类型以提升维护性与开发效率。

cpp
// 检查字符串开头是否包含另外一个字符串的内容
static PyObject* pyStartsWith(PyObject* self, PyObject* args)
{
    const char* str;
    const char* testHeadStr;
    if (!PyArg_ParseTuple(args, "ss", &str, &testHeadStr))
    {
        return NULL;
    }
    // string_view类型需要CPP17+版本 其中starts_with则是20+
    std::string_view cppStrRef {str};
    // 由于C语言并没有bool概念 需要用int来构造PyBool
    return PyBool_FromLong(cppStrRef.starts_with(testHeadStr) ? 1 : 0);
}

static PyMethodDef moduleMethods[] = {
    {"add", pyAdd, METH_VARARGS, ""},
    {"startsWith", pyStartsWith, METH_VARARGS, ""},
    {NULL, NULL, 0, NULL},
};
  • 代码测试
python
import testLib

print(testLib.add(10000, 2000))
print(testLib.startsWith("abc", "ab"))

总结

本章介绍了如何构建一个基础的 Python C 扩展模块,涵盖了从环境准备、CMake 配置、模块入口定义到函数绑定的完整流程。

在实际开发中,建议结合项目需求灵活运用,并注意 Python 与 C/C++ 之间的数据类型和内存管理差异,确保代码稳定高效。

Released under the BSD3 License