CPython v3.12.1 源码学习番外篇(二):编写C++扩展

此番外篇系列的源代码,已在github上传,链接: Rainbowkv/python_ext

概述

番外篇二,我们将利用一个使用dll动态库的C++项目,然后使用Pybind11轻松将其编写为Python的包。它将同时具有一个不错的C/C++工程目录结构和Python包结构,可参考Pytorch的目录。
您如果具有CPython v3.12.1 源码学习番外篇(一):编写C扩展CPython v3.12.1 源码学习番外篇(二)的补充知识:CMake构建C/C++项目的基础,会更快理解本文。
我们会使用到番外篇(二)补充知识构建的rb_math.dll、rb_math.lib和rb_math.h文件,您也可以使用自己的库编写为Python拓展。

一. cpp源文件

pip install pybind11

编辑一个新的.cpp文件,这里为math_dll.cpp。
包含我们的"rb_math.h",以及<pybind11/pybind11.h>,忽略IDE对此的报错。编译是Pybind11的事情。

#include <pybind11/pybind11.h> // IDE可能报错找不到。
#include "rb_math.h"


PYBIND11_MODULE(math, m) {
    m.def("add", &add, "Calculate the sum of two numbers, maybe overflow.");
    m.def("sub", &sub, "Calculate the dif of two numbers, maybe overflow.");
}

PYBIND11_MODULE是Pybind11库的宏定义,用于注册模块。
参数列表:

  • 第一个参数为模块名,我们这里定义为"math";
  • 第二个参数m是方法列表名,与{}的m保持一致即可。

您可以看到{}里有多简单,我们将rb_math.h的两个函数add和sub注册进去即可。
m.def参数列表:

  • 第一个参数指定python中该函数的名称;
  • 第二个参数将函数地址传入(都已在rb_math.h中声明);(细节"&add"和"add"的写法都是函数地址)
  • 第三个参数是该方法的描述,在python中是方法的__doc__属性。

二. setuptools

当前工作目录如下:

├─ build              # 库文件所在目录(不会打进包中)
  ├─ Release
    ├─ rb_math.dll
    ├─ rb_math.lib
├─ include            # 头文件所在目录
  ├─ rb_math.h 
├─ iib
  ├─ rb_math.dll  # 将rb_math.dll复制一份放在这里,等会一同打入包内。
├─ math            # 头文件所在目录
  ├─ __init__.pyi  # 这三个.pyi文件都仅是提供接口说明,可以没有,文末展示了内容
  ├─ add.pyi
  ├─ sub.pyi
  ├─ math_dll.cpp  # 仅利用它生成rainbow/math.pyd文件,调用rb_math.dll完成add和sub功能。
├─ __init__.py    # 您可以在下一步创建
└─ setup.py
  1. 下面是我们的setup.py文件,这里稍微有点长,因为我们会构建一个较规范的python包结构。
  • 重点1:在Pybind11Extension中给出源文件.cpp的路径,依赖的头文件、库文件的路径和库名,前者说明了我们要打那些包,后者是一个字典,告诉setuptools我们的包与当前目录的对应关系;
  • 重点2:理解packages和package_dir,前者说明了我们要打那些包,后者是一个字典,告诉setuptools我们的包与当前目录的对应关系;
  • 细节:lib目录下存放了.dll文件,运行时需要。因此通过package_data告诉setuptools保留"*.dll"文件,默认情况下,setuptools只会保留与Python代码相关的文件。
from setuptools import setup
from pybind11.setup_helpers import Pybind11Extension, build_ext


include_path = "include/"  # rb_math.h所在的目录
dll_path = "build/Release/"  # rb_math.lib和rb_math.dll所在的目录
ext_modules = [
    Pybind11Extension(
        "rainbow.math",  # 这将生成rainbow/math.pyd的文件
        ["math/math_dll.cpp"],
        include_dirs=[include_path],
        library_dirs=[dll_path],
        libraries=["rb_math"],  # 告诉setuptools搜索rb_math.lib文件
    )
]  # 简单的项目就可以这样构造

setup(
    name="rainbow",
    version="0.0.1",
    author="rainbow",
    author_email="rainbowkv@163.com",
    description="A C++ extension package made by rainbow",
    packages=["rainbow", "rainbow.math", "rainbow.lib"],
    package_dir={"rainbow": ".", "rainbow.math": "math", "rainbow.lib": "lib"},  # 前者是包名,后者是包名指定的目录。即告诉setuptools哪个目录就是这个包。这样math才不会在site-packages下与rainbow同级,而是在rainbow目录下
    package_data={"rainbow.lib": ["*.dll"]},  # 指定不想被过滤的非python的文件
    ext_modules=ext_modules,  # 1. 简单的项目就可以这样构造
    cmdclass={"build_ext": build_ext},
    zip_safe=False,
    classifiers=[
        "Programming Language :: Python :: 3",
        "Programming Language :: C++",
        "Operating System :: OS Independent",
    ],
    python_requires='>=3.6',
)
  1. 在真正构建包前我们还有一步,创建__init__.py放在setup.py的同级目录,您需要将lib目录添加系统默认查找动态库的路径中,这个操作放在rainbow/init_.py最为合适,否则运行时系统会找不到dll文件:
# rainbow/_init__.py的关键内容
import os


dll_path = os.path.join(os.path.dirname(__file__), "lib")  # 将lib目录添加系统默认查找动态库的路径
os.add_dll_directory(dll_path) 

这里因为我们构建分发包*.whl,因此用此命令:

python setup.py bdist_wheel

这样就基本成功了,检查下输出,看看目录结构和文件有无缺失。
dist目录被自动创建,下面有一个rainbow-0.0.1-cp38-cp38-win_amd64.whl文件

执行安装命令:

pip install dist\rainbow-0.0.1-cp38-cp38-win_amd64.whl

然后执行命令,之后输入n,不要卸载,pip会告诉我们它在什么目录:

pip uninstall rainbow


看看我们包的结构:


我们直接进入python的交互式命令行,使用此包:

到此包的制作就完成了。您还可以将include文件夹,.lib文件一同打入包,修改setup.py文件即可做到,形成如下包结构:

├─ include           
  ├─ rb_math.h 
├─ iib
  ├─ rb_math.dll  
  ├─ rb_math.lib
├─ math     
  ├─ __init__.pyi  
  ├─ add.pyi
  ├─ sub.pyi
├─ __init__.py    
└─ math.cp38-win_amd64.pyd

相当于同时提供了C/C++的库。
如果您希望包更加规范,能够向外提供接口信息,您可以继续往下看。

三. math/*.pyi的作用

您会发现,任何的IDE中都不能通过索引导航查看关于包的函数接口说明。
因为这是一个pyd文件,已经编译成二进制。虽然可以通过:

print(dir(rainbow.math))

来查看这个模块有哪些接口,但并不方便。
注意到我们现在包的结构是:

├─ iib
  ├─ rb_math.dll  
├─ math           
  ├─ __init__.pyi  
  ├─ add.pyi
  ├─ sub.pyi
├─ __init__.py   
└─ math.cp38-win_amd64.pyd

math目录与math.cp38-win_amd64.pyd,在python代码中都可以表示为rainbow.math。

如果能让编辑器对方法索引时进入math目录对应方法的pyi文件中,
即可查看我们事先在pyi文件中写好函数的说明。
(pyi是python提供文档说明的后缀名文件,不会被python识别,但您的IDE可以索引它)

# math/__init__.pyi
from .add import add
from .sub import sub


__all__ = [
    "add",
    "sub"
]
# math/add.pyi
def add(a: int, b: int) -> int:
    """Calculate the sum of two numbers, maybe overflow.

    Args:
        a (int): _description_
        b (int): _description_

    Returns:
        int: _description_
    """
    pass
# math/sub.pyi
def sub(a: int, b: int) -> int:
    """Calculate the dif of two numbers, maybe overflow.

    Args:
        a (int): _description_
        b (int): _description_

    Returns:
        int: _description_
    """
    pass

注意:本文的.cpp源文件是没有实现真正的加减法的,这里只是为了举出接口文档的例子,实际的add是不传参的。
2024/6/20 0:26补充:
没忍住,来填坑,把加减法实现,修改rb_math.h,rb_math.cpp和math_dll.cpp。
math_dll.cpp添加传参逻辑,返回值pybind11会自动处理。
(pybind11::arg(“x”), pybind11::arg(“y”)这个定义的是python中函数参数的名字)

// rb_math.h
#ifndef RB_MATH_H
#define RB_MATH_H

#include<stdio.h>

#ifndef DDL_IMPORT
#define API __declspec(dllexport)
#else
#define API __declspec(dllimport)
#endif

extern "C"{  // 必须写在这个声明,
             // 否则编译cpp源文件的CPP编译器找到此头文件后会对函数名进行修饰。外面就找不到原始的add。
    API int add(int m, int n);
    API int sub(int m, int n);
}

// API void add();
// API void sub();

#endif
// rb_math.cpp
#include"rb_math.h" //头文件的声明对应修改


int add(int a, int b){
  printf("cpp additing...\n");
  return a + b;
}

int sub(int a, int b){
  printf("cpp subtracting...\n");
  return a - b;
}
// math/math_dll.cpp
#include <pybind11/pybind11.h>
#include "rb_math.h"


PYBIND11_MODULE(math, m) {
    m.def("add", &add, pybind11::arg("x"), pybind11::arg("y"), "Calculate the sum of two numbers, maybe overflow.");
    m.def("sub", &sub, pybind11::arg("x"), pybind11::arg("y"), "Calculate the dif of two numbers, maybe overflow.");
}

rainbow.__init__.py的完整内容:

import os


__all__ = [
    "add",  # 这个符号对应的对象现在还没有真正被加载
    "sub"  # 这个符号对应的对象现在还没有真正被加载
]


def print_in_box(message):
    lines = message.split('\n')
    max_length = max(len(line) for line in lines)
    border = '#' * (max_length + 4)
    
    print(border)
    for line in lines:
        print(f"# {line.ljust(max_length)} #")
    print(border)


print()
# print_in_box("rainbow包初始化开始...")
message = "rainbow package is initializing...\n"

################################################################################
# Load dll
################################################################################
dll_path = os.path.join(os.path.dirname(__file__), "lib")
os.add_dll_directory(dll_path)                                                                      
                                                                                            
from rainbow.math import add, sub  # 这里指定rainbow.math就是导入了pyd,而不是math目录;要导入math目录就是.math
# print_in_box(f"成功添加扩展包的动态库路径, dll_path:\n{dll_path}")
message += f"Added dll path sucessfully, dll_path:\n{dll_path}\n"
################################################################################

# print_in_box("rainbow包初始化完毕.")
message += "Package has initialized."
print_in_box(message)

del print_in_box, dll_path
print()

输出结果:
在这里插入图片描述

下篇编写CUDA扩展

下次我们来编写CUDA代码,调度显卡,最后通过Python调用。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值