简介 这篇文章主要介绍了基于pybind11为C++提供Python接口以及相关的经验技巧,文章约28320字。
每种编程语言都有其擅长的应用领域,使用C++ 可以充分发挥性能优势,而Python 则对使用者更为友好.“小朋友才做选择,我全都要!”.开发者可以将性能关键的部分以C++ 实现,并将其包装成Python 模块.这里基于pybind11 以下列顺序来展示如何实现:
- 示例动态库
pybind11 库依赖管理Python 模块- 语法提示
- 发布包支持
示例环境要求
-
由于pybind11 使用C++11 ,在Windows 环境下需要Visual Studio 2015 及以上版本. -
需要本机安装Python -
需要本机安装CMake 3.15 及以上版本,可以通过Python 的包管理器pip 安装,命令如下: pip install cmake
这里假设使用了Visual Studio 2017 版本,以下的CMake 命令以此为依据.(Linux下大同小异)
示例动态库
这里首先提供一个示例用的动态库来模拟常规的C++ 代码,其目录结构如下:
mylib.hpp mylib.cpp CMakeLists.txt
其中mylib.hpp 内容如下:
#pragma once
#include <string>
int add(int i = 1, int j = 2);
enum Kind {
Dog = 0,
Cat
};
struct Pet {
Pet(const std::string& name, Kind type) : name(name), type(type) { }
std::string name;
Kind type;
};
函数add 的实现在mylib.cpp 中:
#include "mylib.hpp"
int add(int i, int j)
{
return i+j;
}
CMakeLists.txt 的内容也较为简单:
cmake_minimum_required(VERSION 3.15)
#声明工程
project(example
LANGUAGES CXX
VERSION 1.0
)
#创建动态库模块
add_library(mylib SHARED)
target_sources(mylib
PRIVATE mylib.cpp
PUBLIC mylib.hpp
)
set_target_properties(mylib PROPERTIES
WINDOWS_EXPORT_ALL_SYMBOLS True ##自动导出符号
)
现在在源代码目录下执行如下命令可以生成Visual Studio 解决方案来构建:
cmake -S . -B build -G"Visual Studio 15 2017" -T v141 -A x64
解决方案为build/example.sln .
pybind11 库依赖管理
这里使用CMake 的FetchContent 模块来管理pybind11 库,在CMakeLists.txt 相应位置添加如下内容:
#声明工程
project(example
LANGUAGES CXX
VERSION 1.0
)
#pybind11需要C++11
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
#使用目录结构
set_property(GLOBAL PROPERTY USE_FOLDERS ON)
if(NOT DEFINED CMAKE_RUNTIME_OUTPUT_DIRECTORY)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/$<CONFIG>")
endif()
if(NOT DEFINED CMAKE_LIBRARY_OUTPUT_DIRECTORY)
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/$<CONFIG>")
endif()
#下载pybind11并使能
include(FetchContent)
FetchContent_Declare(
pybind11
URL "https://github.com/pybind/pybind11/archive/v2.4.2.tar.gz"
URL_HASH SHA512=05a49f99c1dff8077b05536044244301fd1baff13faaa72c400eafe67d9cb2e4320c77ad02d1b389092df703cc585d17da0d1e936b06112e2c199f6c1a6eb3fc
DOWNLOAD_DIR ${CMAKE_SOURCE_DIR}/download/pybind11
)
FetchContent_MakeAvailable(pybind11)
这样,重新生成解决方案就可以完成pybind11 库的下载和使能了.
需要注意以下几点:
CMAKE_RUNTIME_OUTPUT_DIRECTORY 等变量需要统一设置一下,来确保动态库和 Python 模块文件输出到相同位置,保证动态库正常加载.FetchContent_Declare 的 DOWNLOAD_DIR 是可选的,但是由于 github 访问不稳定,可以指定该路径位置,并手动下载文件到这里,从而避免每次构建都去访问 github .
Python 模块
向源代码目录添加example.cpp ,目录结构类似如下:
mylib.cpp example.cpp CMakeLists.txt
首先修改CMakeLists.txt 来添加Python 模块:
set_target_properties(mylib PROPERTIES
WINDOWS_EXPORT_ALL_SYMBOLS True ##自动导出符号
)
#创建python模块
pybind11_add_module(example)
target_sources(example
PRIVATE example.cpp
)
target_link_libraries(example
PRIVATE mylib
)
重新生成解决方案,则可以看到解决方案中已经有了example 工程,这时库依赖已经配置好,可以正常使用语法提示编写出以下example.cpp 内容:
#include <pybind11/pybind11.h>
#include "mylib.hpp"
namespace py = pybind11;
PYBIND11_MODULE(example, m)
{
m.doc() = "pybind11 example plugin";
//函数:注释、参数名及默认值
m.def("add", &add, "A function which adds two numbers",
py::arg("i") = 1, py::arg("j") = 2);
//导出变量
m.attr("the_answer") = 42;
py::object world = py::cast("World");
m.attr("what") = world;
//导出类型
py::class_<Pet> pet(m, "Pet");
//构造函数及成员变量(属性)
pet.def(py::init<const std::string &, Kind>())
.def_readwrite("name", &Pet::name)
.def_readwrite("type", &Pet::type);
//枚举定义
py::enum_<Kind>(m, "Kind")
.value("Dog", Kind::Dog)
.value("Cat", Kind::Cat)
.export_values();
}
生成解决方案后,在输出目录会出现类似以下内容:
mylib.dll example.cp38-win_amd64.pyd
在输出目录启动命令行,进入Python 交互环境,执行如下指令:
import example
help(example.add)
会得到类似如下输出:
>>> help(example.add)
Help on built-in function add in module example:
add(...) method of builtins.PyCapsule instance
add(i: int = 1, j: int = 2) -> int
A function which adds two numbers
试着调用example.add :
>>> print(example.add())
3
这时我们的Python 模块就开发完成,且能够正常运行了.Visual Studio 支持C++ 与Python 联合调试,当遇到执行崩溃等问题时,可以搜索调试方式,在相应的C++ 代码处断点查看问题所在.
不过这时生成的Python 模块在Visual Studio Code 等IDE 中并没有较好的语法提示,会导致使用者严重依赖文档,这个问题可以通过提供.pyi 文件来解决.
语法提示
C++ 是强类型语言,导出的Python 模块对类型是有要求的,而Python 通过PEP 484 -- Type Hints 支持类型等语法提示,这里可以随着.pyd 提供.pyi 文件来包含Python 模块的各种定义、声明及文档.
在源代码目录添加example.pyi 文件,此时目录结构类似如下:
example.cpp example.pyi CMakeLists.txt
修改CMakeLists.txt 文件使得构建example 时自动复制example.pyi 到相同目录:
target_link_libraries(example
PRIVATE mylib
)
#拷贝类型提示文件
add_custom_command(TARGET example POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${CMAKE_CURRENT_SOURCE_DIR}/example.pyi $<TARGET_FILE_DIR:example>/
)
示例example.pyi 文件内容如下:
import enum
def add(i: int = 1, j: int = 2) -> int:
"""两个数值相加
Args:
i (int, optional): [数值1]. Defaults to 1.
j (int, optional): [数值2]. Defaults to 2.
Returns:
int: [相加的结果]
"""
pass
the_answer = 42
what = "World"
class Kind(enum.Enum):
Dog = 0
Cat = 1
class Pet:
name: str
type: Kind
def __init__(self, name: str, type: Kind):
self.name = name
self.type = type
注意,.pyi 文件只是用来作为语法提示,公开的类型、函数、变量不需要填写真实内容,譬如add 只是书写了函数声明,内容直接使用了pass 来略过.
重新生成解决方案后,在输出目录使用Visual Studio Code 创建使用示例,编码过程中就可以看到完整的语法提示和注释内容.
发布包支持
常规的Python 包都可以使用pip 安装,二进制包则一般被打包成.whl 格式并使用以下命令来安装:
pip install xxx.whl
如果希望支持这种方式,则需要提供setup.py 来支持发布包.
在源代码目录添加setup.py ,此时的目录结构类似如下:
example.cpp CMakeLists.txt setup.py
其中setup.py 内容如下:
import os
import sys
import subprocess
from setuptools import setup, Extension
from setuptools.command.build_ext import build_ext
PLAT_TO_CMAKE = {
"win32": "Win32",
"win-amd64": "x64",
"win-arm32": "ARM",
"win-arm64": "ARM64",
}
def setup_init_py(dir):
all = []
for file in [file for file in os.listdir(dir) if file.endswith(".pyd")]:
all.append('"{}"'.format(file.split('.', 1)[0]))
with open(os.path.join(dir, "__init__.py"), "w") as file:
file.write('__all__=[{}]'.format(','.join(all)))
return
class CMakeExtension(Extension):
def __init__(self, name, sourcedir):
Extension.__init__(self, name, sources=[])
self.sourcedir = os.path.abspath(sourcedir)
class CMakeBuild(build_ext):
def build_extension(self, ext):
extdir = os.path.abspath(os.path.dirname(
self.get_ext_fullpath(ext.name)))
if not extdir.endswith(os.path.sep):
extdir += os.path.sep
cfg = "Debug" if self.debug else "Release"
cmake_generator = os.environ.get("CMAKE_GENERATOR", "")
cmake_args = [
"-DCMAKE_RUNTIME_OUTPUT_DIRECTORY={}".format(extdir),
"-DCMAKE_LIBRARY_OUTPUT_DIRECTORY={}".format(extdir),
"-DPYTHON_EXECUTABLE={}".format(sys.executable),
"-DEXAMPLE_VERSION_INFO={}".format(
self.distribution.get_version()),
"-DCMAKE_BUILD_TYPE={}".format(cfg),
]
build_args = []
if self.compiler.compiler_type != "msvc":
if not cmake_generator:
cmake_args += ["-GNinja"]
else:
single_config = any(
x in cmake_generator for x in {"NMake", "Ninja"})
contains_arch = any(x in cmake_generator for x in {"ARM", "Win64"})
if not single_config and not contains_arch:
cmake_args += ["-A", PLAT_TO_CMAKE[self.plat_name]]
if not single_config:
cmake_args += [
"-DCMAKE_LIBRARY_OUTPUT_DIRECTORY_{}={}".format(
cfg.upper(), extdir),
"-DCMAKE_RUNTIME_OUTPUT_DIRECTORY_{}={}".format(
cfg.upper(), extdir)
]
build_args += ["--config", cfg]
if "CMAKE_BUILD_PARALLEL_LEVEL" not in os.environ:
if hasattr(self, "parallel") and self.parallel:
build_args += ["-j{}".format(self.parallel)]
if not os.path.exists(self.build_temp):
os.makedirs(self.build_temp)
subprocess.check_call(
["cmake", ext.sourcedir] + cmake_args, cwd=self.build_temp
)
subprocess.check_call(
["cmake", "--build", "."] + build_args, cwd=self.build_temp
)
setup_init_py(extdir)
sourcedir = os.path.dirname(os.path.realpath(__file__))
setup(
name="myexample",
version="0.0.1",
author="liff",
author_email="liff.engineer@gmail.com",
description="A example project using pybind11 and CMake",
long_description="",
ext_modules=[CMakeExtension("myexample.impl", sourcedir=sourcedir)],
cmdclass={"build_ext": CMakeBuild},
zip_safe=False,
)
具体setup.py 如何编写可以查阅相关资料,以及代码中的注释,上述setup.py 的内容是可以直接使用的,需要修改的内容均在setup() 中:
setup(
name="myexample",
version="0.0.1",
author="liff",
author_email="liff.engineer@gmail.com",
description="A example project using pybind11 and CMake",
long_description="",
ext_modules=[CMakeExtension("myexample.impl", sourcedir=sourcedir)],
cmdclass={"build_ext": CMakeBuild},
zip_safe=False,
)
声明CMakeExtension 时务必注意,在包名称myexample 后添加.xxxx ,否则生成的发布包内容无法形成文件夹,会被散落到Python 第三方库安装路径site-packages 下,带来不必要的困扰.
完成上述内容后,在源代码目录执行如下命令:
python setup.py bdist_wheel
即可生成相应的.whl 包,这时的目录结构类似以下形式:
-
build -
dist -
myexample-0.0.1-cp38-cp38-win_amd64.whl -
download -
myexample.egg-info -
CMakeLists.txt -
setup.py
其中dist\myexample-0.0.1-cp38-cp38-win_amd64.whl 即为发布包,通过pip 安装:
pip install myexample-0.0.1-cp38-cp38-win_amd64.whl
就可以正常使用myexample 这个Python 模块了.
这里提供一个应用示例共测试使用:
from myexample.example import Pet, Kind, the_answer, what, add
cat = Pet("mycat", Kind.Cat)
dog = Pet("mydog", Kind.Dog)
print(cat.name)
print(dog.name)
print(the_answer)
print(what)
print(add(3, 4))
总结
以上展示了基于pybind11 为C++ 库提供Python 模块涉及到的方方面面,可以按照流程操作熟悉一下.这里并没有详细阐述pybind11 的使用方法,留待后续展开.
参考:caimouse主页
|