前提
因项目需求,需要在C++中调用python,对这方面的一些工具做个简单的介绍。
ctypes
ctypes 是 Python 的外部函数库。它提供了与 C 兼容的数据类型,并允许调用 DLL 或共享库中的函数。可使用该模块以纯 Python 形式对这些库进行封装。
上面是ctypes官方文档给出的介绍,通俗理解来说:ctypes可以加载动态链接库,然后以此调用动态链接库中的函数。也就是说,如果我们有一个.c文件,我们可以将它编译成库,然后在python代码里面使用ctypes加载调用它。
相关代码如下:
- 创建一个
main.c文件,包括三个函数,等会我们要通过调用动态链接库的方式在python中调用这三个函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| #include <stdio.h> #include <stdlib.h>
int add(int a, int b) { return a + b; }
int sum(int *a, int num){ int sum = 0; for(int i=0; i<num; i++){ sum += a[i]; } return sum; }
|
- 将
main.c编译为动态链接库mainlib.dll
1
| gcc -shared -o mainlib.dll main.c
|
- 现在我们的文件夹下便会多出一个
mainlib.dll库文件,接下来我们在python中调用并且使用它。
1 2 3 4 5 6 7 8 9 10 11 12 13
| import ctypes from ctypes import * mainlib = ctypes.CDLL('test/mainlib.dll')
a = ctypes.c_int(1) b = ctypes.c_int(2) print(mainlib.add(a,b))
int_array = (c_int * 3)(1, 2, 3) num = ctypes.c_int(3) print(mainlib.sum(int_array, 3))
|
总结: ctypes可以应用到在python中调用c函数,也就是python调用C,也就是扩展python。
pybind11之前我使用过,当时的场景是:有一个深度学习算子是用c和cuda写的,要把它接入到pytorch中,**相当于是python中调用c**。当时的解决方案是:使用pybind11这个工具将这个算子封装成动态库文件,然后在python端进行加载运行。
在这里,我可以很明确的告诉大家:pybind11可以使我们在python中调用C++(这是pybind11的主要目的和应用),也可以使我们在C++中调用python。 下面给出两个示例。
在python中调用C++
- 安装pybind11
这里我建议使用conda install 的方式安装pybind11,否在后面在C++中会找不到pybind的头文件等。
- 创建main.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13
| #include <pybind11/pybind11.h>
namespace py = pybind11;
int add(int i, int j){ return i+j; }
PYBIND11_MODULE(example, m) { m.doc() = "pybind11 example plugin";
m.def("add", &add, "A function that adds two numbers"); }
|
可以看到,在main.c中定义了一个add函数。后面几行添加了pybind接口的代码。
- 生成动态链接库
生成动态链接库这一部分,很多教程中使用的都是一个setup.py,这里我使用从官网得到的命令行生成.so文件。
1
| c++ -O3 -Wall -shared -std=c++11 -fPIC $(python3 -m pybind11 --includes) main.cpp -o example$(python3-config --extension-suffix)
|
运行完这条指令后,可以看到文件夹中多了一个以example开头的.so文件。
- 在python中调用该动态链接库
以上就是使用pybind11在python中调用c++的全流程。
在C++中调用python
这一部分互联网上资源很少,我没有找到一个完整的demo,最后从pybind11的官网 找到了一些demo,这里进行展示。
- 准备c++环境
因为我是使用cmake编译代码,所以第一步要找到pybind11的头文件,也就是确保CmakeLists.txt文件正确。下面是我的cmake文件。
1 2 3 4 5 6 7
| cmake_minimum_required(VERSION 3.16) project(main)
find_package(pybind11 REQUIRED)
add_executable(main main.cpp) target_link_libraries(main pybind11::embed)
|
- demo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| #include <pybind11/embed.h> #include <iostream> namespace py = pybind11;
int main(){ py::scoped_interpreter guard{};
py::print("hello, world!");
py::exec(R"( kwargs = dict(name="World", number=42) message = "Hello, {name}! The answer is {number}".format(**kwargs) print(message) )");
py::module_ sys = py::module_::import("sys"); py::print(sys.attr("path"));
py::module_ calc = py::module_::import("calc"); py::object result = calc.attr("add")(1,2); int n = result.cast<int>(); std::cout<<"n = "<<n<<std::endl;
return 0; }
|
Cython
这里先强调一点:Cython和CPython是完全不同的两个东西以及这篇文章。
Cython是一门结合了C和Python的编程语言(Cython是python的超集),接下来我们给出Cython几种不同的作用,但是无论如何,在linux下Cython最后都会生成一个.so文件。
加快python速度
我们有一个python写的斐波那契数列,但是运行速度太慢,因为Cython中有C语言的特性,所以我们可以使用Cython语言重写斐波那契数列,然后编译为动态链接库,然后在python代码中使用。
代码如下:
1.斐波那契数列原始的python代码:
1 2 3 4 5 6
| def fib(n): a, b = 0.0, 1.0 for i in range(n): a, b = a + b, a return a
|
用Cython重写的斐波那契数列,文件后缀名为.pyx:
1 2 3 4 5 6 7
| def fib(int n): cdef int i cdef double a = 0.0, b = 1.0 for i in range(n): a, b = a + b, a return a
|
- 编译
fib.pyx文件为动态链接库.so
这里有两种编译方式,一种是使用setup.py自动进行编译,一种是手动进行编译。
1 2 3 4 5
| from distutils.core import setup from Cython.Build import cythonize
setup(ext_modules = cythonize("fib.pyx"))
|
然后在命令行运行`python setup.py build_ext --inplace` 便会在同级目录下生成一个以`fib`开头的动态链接库以及一个`fib.c`文件,这个`fib.c`文件就是`fib.pyx`完全转为`c`代码后结果。
- 手动编译
- 第一步:在命令行运行`cython fib.pyx`,会生成`fib.c`
- 使用gcc对`fib.c`编译生成动态链接库: `gcc -fPIC -shared -I ~/miniconda3/include/python3.11/ fib.c -o fib.so`。注意这里python include的路径需要你自己更换为自己环境的路径。
这样在第2步,我们就生成了动态链接库.so文件。
- 在python代码中使用这个动态链接库
1 2 3
| import fib
print(fib.fib(100))
|
以上就是Cython工作的大体流程。这里要注意的是:我的介绍只是一点点入门知识,Cython还是很博大精深的。
在C中调用python代码
上面我们已经说过,Cython是python的超集,所以如果我们有一个python脚本或者模块,想要在C语言环境中调用它,那么可以使用cython对这个py文件进行编译生成动态链接库,然后在C语言中调用它即可。
注:这一种方式博主没有亲自测试过
调用Python的原生C API
这是最暴力的一种方法,我们知道,python这个语言也有C的API,所以我们可以直接在C语言代码中使用这些API来调用python模块,下面是一个简单的示例。
- 我们拥有的
my_modules.py文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| def add(a, b): print(a + b) return a + b
def helloworld(s): print("hello " + s)
class A: def __init__(self, a, b) -> None: self.first = a self.second = b
def add(self): print(self.first+self.second) return "hello world"
|
可以看到,有两个函数(一个做求和,一个输出"hello world")和一个类。
- 构建C++的环境
我是使用cmake进行编译程序的,所以要配置好CMakeLists.txt,配置如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| cmake_minimum_required(VERSION 3.16) project(CallPython)
find_package (Python COMPONENTS Interpreter Development) message(STATUS "Python_VERSION: ${Python_INCLUDE_DIRS}") message(STATUS "python_LIBRARIES: ${Python_LIBRARIES}")
include_directories( ${Python_INCLUDE_DIRS} )
add_executable(call_python call_python.cpp)
target_link_libraries(call_python ${Python_LIBRARIES})
|
- 创建
call_python.cpp文件,文件内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
| #include <iostream> #include <Python.h>
int main(int argc, char** argv){ Py_Initialize(); PyRun_SimpleString("import sys"); PyRun_SimpleString("sys.path.append('/home/wjq/workspace/test1')");
PyObject* pModule = PyImport_ImportModule("my_modules"); PyObject* pFunc = PyObject_GetAttrString(pModule, "add"); PyObject* args = PyTuple_New(2); PyTuple_SetItem(args, 0, Py_BuildValue("i", 1)); PyTuple_SetItem(args, 1, Py_BuildValue("i", 10)); PyObject* pRet = PyObject_CallObject(pFunc, args); if (pRet) { long result = PyLong_AsLong(pRet); std::cout << "result:" << result << std::endl ; }
pFunc = PyObject_GetAttrString(pModule, "helloworld"); PyObject* str = Py_BuildValue("(s)", "python"); PyObject_CallObject(pFunc, str);
PyObject* pDict = PyModule_GetDict(pModule); PyObject* pClassA = PyDict_GetItemString(pDict, "A"); PyObject* pConstruct = PyInstanceMethod_New(pClassA); PyObject * pInsA = PyObject_CallObject(pConstruct, args); PyObject* result = PyObject_CallMethod(pInsA, "add", nullptr); if(result != nullptr){ char * str_result; PyArg_Parse(result, "s", &str_result); printf("Result: %s\n", str_result); Py_DECREF(result); }
Py_Finalize();
}
|
结果如下所示:
1 2 3 4 5
| 11 result:11 hello python 11 Result: hello world
|
Python的C API有很多,这里我们只是用了几个,关于更多的API,请参考官网。
参考链接
- https://www.52txr.cn/2023/CPytonCython.html
- https://www.cnblogs.com/traditional/p/13196509.html
- https://chend0316.github.io/backend/cython/#第1章-cython的安装和使用
- https://blog.csdn.net/u011722929/article/details/114871365
- https://www.hbblog.cn/python%26C%2B%2B/python和C的交互/#31-pythonapi
- https://zhuanlan.zhihu.com/p/79896193
- https://blog.csdn.net/qq\_42688495/article/details/120563844