Cython学习笔记2:Cython调用C语言函数库
Cython
Cython 是一个用于将 Python 代码转换为 C 语言扩展模块的编程语言。它允许你在 Python 中编写 C 风格的代码,从而提高性能,尤其是在需要大量计算的情况下。通过将 Python 代码与 C 代码混合使用,Cython 既保留了 Python 的简洁性,又能提升程序的执行速度。
Cython 的核心特点:
- 提高性能:Cython 可以将 Python 代码编译成 C 语言代码,然后生成高效的 C 扩展模块。它适用于需要频繁计算的任务,尤其是数值计算和循环处理。
- 与 C 代码交互:Cython 允许直接调用 C 函数和访问 C 数据结构,从而减少了 Python 和 C 之间的接口调用开销。
- 简化 C 接口:与直接使用 C 语言不同,Cython 通过 Python 风格的语法简化了 C 接口的使用,开发者可以快速将 Python 代码提升到 C 代码的性能。
- 无缝集成:Cython 代码可以与现有的 Python 代码无缝集成,开发者不需要重写整个程序,通常只需要优化性能瓶颈部分。
Cython调用C语言函数库
以Windows操作系统举例,使用Visual Studio 2022创建C语言函数库
1. Cython调用C语言函数库原理
Cython加速Python运行速度原理是:把 python 代码转化为 c 语言代码,然后再编译成动态链接库,最后使用 python 程序调用这个库,示意图如下
由于是利用 Cython 编译出的动态链接库,因此 python 程序可以直接调用,但是如果调用外部的(不是Cython 编译出的)动态链接库,需要执行如下过程:
把 .pyx 文件,文件中定义了要使用的外部库中的函数,然后转化为 c 语言代码,然后再编译成动态链接库,最后使用 python 程序调用这个库
2. 创建C语言动态链接库
打开VS2022,选择创建一个新的项目
找到动态链接库
填写合适的项目名称和路径
把原来项目中的cpp文件修改c文件,然后创建一个要调用的函数头文件和.c文件,原来项目中的文件不进行任何修改
DllMain 是 DLL 的入口函数,每次 DLL 被加载或卸载时,都会调用这个函数。它有三个参数:
- HMODULE hModule:该参数是 DLL 模块的句柄,表示当前加载的 DLL。通过这个句柄可以访问 DLL 的信息。
- DWORD ul_reason_for_call:表示触发 DllMain 的原因,可能的值有:
1.DLL_PROCESS_ATTACH:DLL 被加载到进程中时。
2.DLL_THREAD_ATTACH:线程被创建时。
3.DLL_THREAD_DETACH:线程被销毁时。
4.DLL_PROCESS_DETACH:DLL 被卸载时。 - LPVOID lpReserved:保留参数,通常在处理过程中没有使用,但它允许未来的扩展。
fun.h
内容如下
#ifndef __FUN_H__
#define __FUN_H__
__declspec(dllexport) double calculate_pi(int count);
#endif
fun.c
内容如下
#include "pch.h"
#include "fun.h"
__declspec(dllexport) double calculate_pi(int count) {
double pi = 3.0;
int sign = 1;
for (int i = 2; i < count * 2; i += 2) {
double term = sign * 4.0 / (i * (i + 1) * (i + 2));
pi += term;
sign *= -1;
}
return pi;
}
这个函数用来计算圆周率 π,使用Nilakantha 级数来计算 π 值,计算公式如下:
接下来建立动态链接库,选择Build -> Build DLLTest
信息如下展示:
之后进入到项目所在的目录,进入x64/Debug/目录下,就可以看到创建的dll了
2. 使用Cython在Python中使用DLL
这里使用的是Pycharm创建了一个项目,把之前产生的.lib和.dll复制过来,写的fun的代码复制过来
创建 fun_wrapper.pyx
文件,代码如下
# cython: language_level=3
cdef extern from "./fun.h":
cpdef double calculate_pi(int count)
这里只需要写需要调用的函数就可以了
Cython 语言使用标准的 C 语法来表示 C 类型,包括指针。它提供了所有标准的 C 类型,详细可以查看这个链接
创建 setup.py
文件,代码如下,用于链接外部的库
from Cython.Build import cythonize
from setuptools import Extension, setup
extern = Extension(
"fun", ["fun_wrapper.pyx"],
libraries=["DLLTest"], library_dirs=["./"]
)
setup(name='fun_wrapper', ext_modules=cythonize(extern))
在此目录下,执行命令
python setup.py build_ext --inplace
执行成功后,就会在当前目录下面出现 .pyd 文件
创建一个 main.py
,测试一下,代码如下:
import time
import os
# 将当前目录添加到 PATH 中
os.environ['PATH'] = os.getcwd() + os.pathsep + os.environ['PATH']
import fun
def calculate_pi(count: int) -> float:
pi = 3.0
sign = 1
for i in range(2, count * 2, 2):
term = sign * 4.0 / (i * (i + 1) * (i + 2))
pi += term
sign *= -1
return pi
def main():
start_time = time.time()
print(calculate_pi(20_000_000))
end_time = time.time()
print('Time:', end_time - start_time)
start_time = time.time()
print(fun.calculate_pi(20_000_000))
end_time = time.time()
print('Time:', end_time - start_time)
if __name__ == '__main__':
main()
运行结果如下:
3.141592653589787
Time: 4.7831432819366455
3.49635588787317
Time: 0.159468412399292
可以看到外部库在python中调用成功了,而且运行速度比在python中定义的函数要快
如果是把c++文件打包成动态链接库,不要忘记在函数最前面,加入 extern "C"
如下fun.cpp
#include "pch.h"
#include "fun.h"
extern "C" __declspec(dllexport) double calculate_pi(int count) {
double pi = 3.0;
int sign = 1;
for (int i = 2; i < count * 2; i += 2) {
double term = sign * 4.0 / (i * (i + 1) * (i + 2));
pi += term;
sign *= -1;
}
return pi;
}