此番外篇系列的源代码,已在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
- 下面是我们的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',
)
- 在真正构建包前我们还有一步,创建__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调用。