Python是脚本语言,可以做的事情非常的多,本文将要讲解使用Python来调用C/C+库。
关于C/C++库的创建请看笔者以前的文章,这里只给出Window下的创建过程。
1 Windows创建动态库
1.1生成动态库
1.首先创建一个动态库工程。
取消预编译头,这里选择DLL。
2.创建头文件(.h)以及源文件(.cpp)
DynamicLib.h
#ifndef _DYNAMIC_LIB_H_
#define _DYNAMIC_LIB_H_
#include <iostream>
#define DLL_EXPORTS
#ifdef DLL_EXPORTS
#define DLL_API extern "C" __declspec(dllexport)
#else
#define DLL_API extern "C" __declspec(dllimport)
#endif
namespace DynamicLib
{
DLL_API void print_hello();
}
#endif // _DYNAMIC_LIB_H_
DynamicLib.cpp
#include "DynamicLib.h"
DLL_API void DynamicLib::print_hello()
{
std::cout << "Hello world!" << std::endl;
}
【注1】__declspec(dllexport)是导出符号,也就是定义需要导出函数的dll中给导出函数的函数声明前面加上导出符号,表示该方法可以导出给其他DLL或者exe使用。
【注2】C++的代码加extern “C”,是为了保证编译时生成的函数名不变,这样动态调用dll时才能找到相应的函数。
3.选择release生成解决方案,这里选择64位。
最后生成的库如下图所示:
好了,动态库就创建好了。
1.2 dumpbin工具的使用
dumpbin.exe为Microsoft COFF二进制文件转换器,它显示有关通用对象文件格式(COFF)二进制文件的信息。可用使用dumpbin检查COFF对象文件、标准COFF对象库、可执行文件和动态链接库等。
dumpbin.exe所在路径是VS安装目录\VC\bin\dumpbin.exe,也可以通过开始菜单里面的Visual Studio开发人员命令提示来运行。
dumpbin使用方式:
dumpbin [选项] [文件名]
其中多个选项间用空格分开,多个文件名间也用空格分开,文件名可以为后缀为.obj、.lib、.dll、.exe,如果没有指定任何输入文件,它将列出所有的选项。
选项说明:参数的使用可以用”-”或者”/”(如-ALL等于/ALL)后面跟选项名。有些选项可以在选项名后接”:”。使用空格或制表符(Tab)分割命令选项。选项名,关键字和文件名是不区分大小写的。大多数的参数可以应用于所有的二进制文件,有少部分参数只能用于特定的文件。
(1)/ALL :此选项显示除代码反汇编外的所有可用信息。使用/DISASM显示反汇编。可以与/ALL一起使用/RAWDATA:NONE来省略文件的原始二进制详细资料。
(2)/ARCHIVEMEMBERS:此选项显示有关库成员对象的最少信息。 (3)、/CLRHEADER file:其中file为用/clr生成的图像文件。CLRHEADER显示有关在任何托管程序中使用的.net头的信息。输出显示.net头及其中各节的位置和大小(以字节计算)。
(3)/DIRECTIVES:此选项转储图像中由编译器生成的.directive节。
(4)/DEPENDENTS:转储图像从中导入函数的DLL的名称。不要转储导入函数名。
(5)/DISASM:此选项显示代码段的反汇编,如果出现在文件中则使用符号。
(6)/EXPORTS:此选项显示从可执行文件或DLL导出的所有定义。
(7)/FPO:此选项显示框架指针优化(FPO)记录。
(8)/HEADERS:此选项显示文件头和每节的头。当用于库时,显示每个成员对象的头。
(9)/IMPORTS[:file]:此选项显示导入到可执行文件或DLL的DLL列表(静态链接的和延迟加载)和上述每个DLL的各个导入。可选file规范允许指定仅显示某个DLL的导入。
(10)/LINENUMBERS:此选项显示COFF行号。如果对象文件是用程序数据库(/Zi)、C7兼容(/Z7)或仅限行号(/Zd)编译的,则它包含行号。如果可执行文件或DLL是与生成调试信息(/DEBUG)链接的,则它包含COFF行号。
(11)/LINKERMEMBER[:{1|2}]:此选项显示库中定义的公共符号。指定参数1将按对象顺序显示符号及其偏移量。指定参数2将显示对象的偏移量和索引号,然后按字母顺序列车这些符号及每个符号的对象索引。若要两个输出都获得,指定不带数字参数的/LINKERMEMBER。
(12)/LOADCOMFIG:此选项转储IMAGE_LOAD_CONFIG_DIRECTORY结构,此结构是由WindowsNT加载程序使用并在WIINNT.H中定义的可选结构。
(13)/OUT:filename:此选项指定输出的filename。默认情况下,DUMPBIN将信息显示到标准输出。
(14)/PDBPATH[:VERBOSE]filename:filename为要为其查找匹配.pdb文件的.dll或.exe文件名。VERBOSE(可选)为报告曾尝试在其中定位.pdb文件的所有目录。/PDBPATH将沿调试器搜索.pdb文件的同一路径搜索计算机,并将报告那些.pdb文件(若有)和filename中指定的文件相对应。
(15)/RAWDATA[:{1|2|4|8|NONE}[,number]]:此选项显示文件中每节的原始内容。参数说明:1,默认值,内容以十六进制字节显示,如果内容具有打印的表示形式,则还显示为ASCII字符;2,内容显示为十六进制的2字节值;4,内容显示为十六进制的恶4字节值;8,内容显示为十六进制的8字节值;NONE,取消显示原始数据,此参数对控制/ALL输出很有用;number,显示的行被设置为每行具有number个值的宽度。
(16)/RELOCATIONS:此选项显示对象或图像中的任何重定位。
(17)/SECTION:section:此选项限制与指定的section有关的信息的输出。
(18)/SUMMARY:此选项显示有关节的最少信息(包括总大小)。如果未指定其它选项,则此选项为默认值。
(19)/SYMBOLS:此选项显示COFF符号表。符号表存在于所有对象文件中。而对于图像文件,只有当它是与/DEBUG链接时,它才包含COFF符号表。
(20)/UNWINDINFO:在程序图像(例如exe和dll)中转储结构化异常处理(SHE)表的展开描述符。/UNWINDINFO仅适用于IA64图像。
完整的选项可移步至MSDN文档查看。
本文主要用dumpbin查看动态库函数:
#dumpbin -exports xxx.dll
从上图可以看出动态库导出了1个函数,与上面代码中的导出函数一致。
值得注意的是,要想导出动态库,就需要加extern “C” 语句,否则C++会按照自己的规则篡改函数的名称。C++支持函数重载,就是在函数名字改编阶段记录下函数的相关参数信息。C++标准并没有定义名字改编的标准,因此会导致不同编译器编译出来的动态库不能通用。
而C标准规定了名字改编的标准,extern "C"告诉编译器在编译代码是按照C的标准进行编译。我相信很多做嵌入式的朋友都看到过extern "C"语句。。
另外需要注意,extern "C"修饰的函数进行了重载,则会在编译时报错,因为C语言并不支持函数的重载。
2 Python调用C/C++动态库
Python调用动态库是通过ctypes这个内建的包。ctypes提供了C的兼容数据类型,允许调用DLL或者共享库中的函数。通过该模块能可以使用Python的代码对这些库进行调用,非常方便。
ctypes 适合于“中轻量级”的Python C/C++混合编程。特别是遇到第三方库提供动态链接库和调用文档,且没有编译器或编译器并不互相兼容的场合下,使用ctypes特别方便。值得注意的是,对于某种需求,在Python本身就可以实现的情况下(例如获取系统时间、读写文件等),应该优先使用Python自身的功能而不要使用操作系统提供的API接口,否则你的程序会丧失跨平台的特性。
Python类型和C语言类型的对应关系:
该表格列举了ctypes、c和python之间基本数据的对应关系,在定义函数的参数和返回值时,需记住几点:
1.必须使用ctypes的数据类型。
2.参数类型用关键字argtypes定义,返回类型用restype定义,其中argtypes必须是一个序列,如tuple或list,否则会报错。
3.若没有显式定义参数类型和返回类型,Python默认为int型。Python在调用动态库中的函数时需要指定函数的参数类型和返回值类型。通过Objdll._FuncPtr.restype来指定动态库函数的返回值类型,通过Objdll._FuncPtr.argtypes来指定动态库函数的参数类型,Objdll._FuncPtr.argtypes的类型为turple,包含动态库函数的参数类型列表,指定的参数类型必须为C/C++中参数类型所对应的ctypes类型。
好了,下面直接看Python调用动态库例子吧。
import platform
import ctypes
suffixNmae = {
"Windows":".dll",
"Linux":".so"
}
libPath = './lib/x64/DynamicLib' + suffixNmae[platform.system()]
# 加载动态库
objDll = ctypes.cdll.LoadLibrary(libPath)
if __name__ == '__main__':
## 调用print_hello
objDll.print_hello()
运行结果如下:
结果和使用C是一样的。
下面以不同的参数类型来讲解如何使用Python代用动态库。
2.1值类型
对于参数类型和返回值类型都为值类型的动态库函数,操作相对简单,只需要指定对应的参数和返回值ctype类型即可进行调用。
C/C++代码:
class Math {
public:
int add(int a, int b);
};
int Math::add(int a, int b)
{
return a + b;
}
C++的函数调用使用需要extern "C"来辅助,也就是说还是只能调用C函数,不能直接调用方法,但是能解析C++方法。不是用extern “C”,构建后的动态链接库没有这些函数的符号表。
//C math
Math mathObj;
DLL_API int add(int a, int b)
{
return mathObj.add(a, b);
}
Python代码:
import platform
import ctypes
suffixNmae = {
"Windows":".dll",
"Linux":".so"
}
libPath = './lib/x64/DynamicLib' + suffixNmae[platform.system()]
# 加载动态库
objDll = ctypes.cdll.LoadLibrary(libPath)
if __name__ == '__main__':
# 定义函数参数
nA = ctypes.c_int(2)
nB = ctypes.c_int(3)
# 指定函数返回值类型
objDll.add.restype = ctypes.c_int
# 指定函数的参数类型
objDll.add.argtypes = (ctypes.c_int, ctypes.c_int, )
# 调用函数
res = objDll.add(nA, nB)
print('C : sum = ', res)
运行结果如下:
为了简单,下面的代码将使用C编写。
2.2指针类型
创建ctypes类型的指针需要借助三个相关的函数:
byref相当于C的取地址符号,在参数传递时可以通过byref传递函数的指针。pointer的POINTER的区别是,pointer返回的是一个实例,而POINTER返回的是一个类型。
【例1】
C/C++代码:
DLL_API int max(uint32_t a, uint32_t b, uint32_t *maxNum)
{
*maxNum = a > b ? a : b;
return 0;
}
Python代码:
import platform
import ctypes
suffixNmae = {
"Windows":".dll",
"Linux":".so"
}
libPath = './lib/x64/DynamicLib' + suffixNmae[platform.system()]
# 加载动态库
objDll = ctypes.cdll.LoadLibrary(libPath)
if __name__ == '__main__':
# 定义函数参数
nA = ctypes.c_uint32(2)
nB = ctypes.c_uint32(3)
nMax = ctypes.c_uint32(0)
# 指定函数返回值类型
objDll.max.restype = ctypes.c_int
# 指定函数的参数类型
objDll.max.argtypes = (ctypes.c_uint32, ctypes.c_uint32, ctypes.POINTER(ctypes.c_uint32), )
# 调用函数
res = objDll.max(nA, nB, ctypes.byref(nMax))
maxValue = nMax.value
print('C : max = ', maxValue)
运行结果如下:
【例2】求和
C/C++代码:
//DLL_API int sum(uint32_t nArr[], uint32_t nLength, uint32_t *nSum)
DLL_API int sum(uint32_t *nArr, uint32_t nLength, uint32_t *nSum)
{
if (nArr == nullptr)
{
return -1;
}
uint32_t i;
*nSum = 0;
for (i = 0; i < nLength; i++)
{
*nSum += nArr[i];
}
return 0;
}
Python代码:
import platform
import ctypes
import numpy as np
suffixNmae = {
"Windows":".dll",
"Linux":".so"
}
libPath = './lib/x64/DynamicLib' + suffixNmae[platform.system()]
# 加载动态库
objDll = ctypes.cdll.LoadLibrary(libPath)
if __name__ == '__main__':
data = np.array([[0, 1, 2, 3]], dtype=np.uint32)
# 定义函数参数
nArr = data.ctypes.data_as(ctypes.POINTER(ctypes.c_uint32))
nLength = ctypes.c_uint32(4)
nSum = ctypes.c_uint32(0)
# 指定函数返回值类型
objDll.sum.restype = ctypes.c_int
# 指定函数的参数类型
objDll.sum.argtypes = (ctypes.POINTER(ctypes.c_uint32), ctypes.c_uint32, ctypes.POINTER(ctypes.c_uint32), )
# 调用函数
res = objDll.sum(nArr, nLength, ctypes.byref(nSum))
sumValue = nSum.value
print('C : sum = ', sumValue)
运行结果如下:
Python调用sum函数时,第一个参数需要传递指针,而以下语句返回就是指针的地址:
nArr = data.ctypes.data_as(ctypes.POINTER(ctypes.c_uint32))
当然还有以下方式:
import platform
import ctypes
import numpy as np
suffixNmae = {
"Windows":".dll",
"Linux":".so"
}
libPath = './lib/x64/DynamicLib' + suffixNmae[platform.system()]
# 加载动态库
objDll = ctypes.cdll.LoadLibrary(libPath)
if __name__ == '__main__':
data = np.array([[0, 1, 2, 3]], dtype=np.uint32)
# 定义函数参数
nArr = data.ctypes.data_as(ctypes.POINTER(ctypes.c_uint32)).contents
print('nArr : ', nArr)
nLength = ctypes.c_uint32(4)
nSum = ctypes.c_uint32(0)
# 指定函数返回值类型
objDll.sum.restype = ctypes.c_int
# 指定函数的参数类型
objDll.sum.argtypes = (ctypes.POINTER(ctypes.c_uint32), ctypes.c_uint32, ctypes.POINTER(ctypes.c_uint32), )
# 调用函数
res = objDll.sum(ctypes.byref(nArr), nLength, ctypes.byref(nSum))
sumValue = nSum.value
print('C : sum = ', sumValue)
运行结果如下:
【例3】
C/C++代码:
//sort
DLL_API int bubble_sort(uint32_t *nOldArr, uint32_t nLen, uint32_t *nNewArr)
{
int i, j, temp;
for (i = 0; i < nLen; i++)
{
nNewArr[i] = nOldArr[i];
}
for (i = 0; i < nLen - 1; i++)
{
for (j = 0; j < nLen - 1 - i; j++)
{
if (nNewArr[j] > nNewArr[j + 1])
{
temp = nNewArr[j];
nNewArr[j] = nNewArr[j + 1];
nNewArr[j + 1] = temp;
}
}
}
return 0;
}
Python代码:
import platform
import ctypes
import numpy as np
suffixNmae = {
"Windows":".dll",
"Linux":".so"
}
libPath = './lib/x64/DynamicLib' + suffixNmae[platform.system()]
# 加载动态库
objDll = ctypes.cdll.LoadLibrary(libPath)
if __name__ == '__main__':
data = np.array([[10, 2, 15, 3, 56]], dtype=np.uint32)
# 定义函数参数
nOldArr = data.ctypes.data_as(ctypes.POINTER(ctypes.c_uint32)).contents
print('nOldArr : ', nOldArr)
nLen = ctypes.c_uint32(5)
nNewArr = (5 * ctypes.c_uint32)()
# 指定函数返回值类型
objDll.bubble_sort.restype = ctypes.c_int
# 指定函数的参数类型
objDll.bubble_sort.argtypes = (ctypes.POINTER(ctypes.c_uint32), ctypes.c_uint32, ctypes.POINTER(ctypes.c_uint32), )
# 调用函数
res = objDll.bubble_sort(ctypes.byref(nOldArr), nLen, nNewArr)
print(nNewArr[0], ' ', nNewArr[1], ' ', nNewArr[2], ' ', nNewArr[3], ' ', nNewArr[4])
运行结果如下:
C代码的最后一个参数是一个数组,Python中就需要构建一个数组,将其地址传到C函数中。
2.3结构体类型
【例1】
C/C++代码:
//结构体
typedef struct {
int nX;
int nY;
int nZ;
}STPoint;
DLL_API int sum_square(STPoint *stPoint, uint32_t *nSum)
{
if (stPoint == nullptr)
{
return -1;
}
*nSum = ((stPoint->nX) * (stPoint->nX) + (stPoint->nY) * (stPoint->nY) + (stPoint->nZ) * (stPoint->nZ));
return 0;
}
Python代码:
import platform
import ctypes
suffixNmae = {
"Windows":".dll",
"Linux":".so"
}
libPath = './lib/x64/DynamicLib' + suffixNmae[platform.system()]
# 加载动态库
objDll = ctypes.cdll.LoadLibrary(libPath)
class STPoint(ctypes.Structure):
_fields_ = [("nX", ctypes.c_int), ("nY", ctypes.c_int), ("nZ", ctypes.c_int)]
if __name__ == '__main__':
stPoint = STPoint()
stPoint.nX = 2
stPoint.nY = 3
stPoint.nZ = 4
nSum = ctypes.c_uint32()
# 指定函数返回值类型
objDll.sum_square.restype = ctypes.c_int
# 指定函数的参数类型
objDll.sum_square.argtypes = (ctypes.POINTER(STPoint), ctypes.POINTER(ctypes.c_uint32), )
# 调用函数
res = objDll.sum_square(ctypes.byref(stPoint), ctypes.byref(nSum))
sumSquare = nSum.value
print('C : sum_square = ',sumSquare)
运行结果如下:
【例2】
C/C++代码:
typedef struct {
int nMax;
int nMin;
}STMaxMin;
DLL_API int max_min(int *nArr, uint32_t nLength, STMaxMin *stMaxMin)
{
if (nArr == nullptr)
{
return -1;
}
int i;
stMaxMin->nMax = nArr[0];
stMaxMin->nMin = nArr[0];
for (i = 0; i < nLength; i++)
{
if (nArr[i] > stMaxMin->nMax)
{
stMaxMin->nMax = nArr[i];
}
if (nArr[i] < stMaxMin->nMin)
{
stMaxMin->nMin = nArr[i];
}
}
return 0;
}
Python代码:
import platform
import ctypes
import numpy as np
suffixNmae = {
"Windows":".dll",
"Linux":".so"
}
libPath = './lib/x64/DynamicLib' + suffixNmae[platform.system()]
# 加载动态库
objDll = ctypes.cdll.LoadLibrary(libPath)
class STMaxMin(ctypes.Structure):
_fields_ = [("nMax", ctypes.c_int), ("nMin", ctypes.c_int)]
if __name__ == '__main__':
data = np.array([[10, 2, 15, 3, 56]], dtype=np.int)
nArr = data.ctypes.data_as(ctypes.POINTER(ctypes.c_int))
nLength = ctypes.c_uint32(5)
stMaxMin = STMaxMin()
# 指定函数返回值类型
objDll.max_min.restype = ctypes.c_int
# 指定函数的参数类型
objDll.max_min.argtypes = (ctypes.POINTER(ctypes.c_int), ctypes.c_uint32, ctypes.POINTER(STMaxMin), )
# 调用函数
res = objDll.max_min(nArr, nLength, ctypes.byref(stMaxMin))
print('C : max = ',stMaxMin.nMax, 'min = ',stMaxMin.nMin )
运行结果如下:
Python中调用dll中方法的一般步骤:
1.使用extern c关键字和__declspec(dllexport)以及__declspec(dllimport)对dll进行包装。
2.使用ctypes库加载动态库。
3.根据C代码中数据类型和ctypes中类型的对应关系指定动态库函数的返回值类型和参数类型。
4.调用动态库中的函数。
5.对函数的返回结果进行转换成python中的类型进行使用。
【注】C语言中没有引用类型,如果动态库是由C++编写存在引用类型参数的函数,需要先用指针类型包装成C动态库。
3 Python与C/C++速度比较
很多朋友都会说,Python调用C库,这不是多此一举嘛,直接用Python写不就好啦,或者直接用C不就好啦。在回答这个问题之前,这里先给一个例子来说明,也就是前面的排序算法。
直接看代码:
# -*- coding: utf-8 -*-
"""
@file main.py
@author BruceOu
@version V1.0
@date 2021-08-06
@blog https://blog.bruceou.cn/
@Official Accounts 嵌入式实验楼
@brief main
"""
import platform
import ctypes
import numpy as np
import time
suffixNmae = {
"Windows":".dll",
"Linux":".so"
}
libPath = './lib/x64/DynamicLib' + suffixNmae[platform.system()]
def bubbleSort(arr):
n = len(arr)
# 遍历所有数组元素
for i in range(n):
# Last i elements are already in place
for j in range(0, n-i-1):
if arr[j] > arr[j+1] :
arr[j], arr[j+1] = arr[j+1], arr[j]
# 加载动态库
objDll = ctypes.cdll.LoadLibrary(libPath)
isCpp = False
if __name__ == '__main__':
arr = np.random.randint(low=1, high=100000, size=10000, dtype=np.uint32)
data = np.array(arr, dtype=np.uint32)
start = time.time()
if(isCpp):
length = data.size
# 定义函数参数
nOldArr = data.ctypes.data_as(ctypes.POINTER(ctypes.c_uint32)).contents
nLen = ctypes.c_uint32(length)
nNewArr = (length * ctypes.c_uint32)()
# 指定函数返回值类型
objDll.bubble_sort.restype = ctypes.c_int
# 指定函数的参数类型
objDll.bubble_sort.argtypes = (ctypes.POINTER(ctypes.c_uint32), ctypes.c_uint32, ctypes.POINTER(ctypes.c_uint32), )
# 调用函数
res = objDll.bubble_sort(ctypes.byref(nOldArr), nLen, nNewArr)
print(nNewArr[0], ' ', nNewArr[1], ' ', nNewArr[2], ' ', nNewArr[3], ' ', nNewArr[4])
else:
bubbleSort(arr)
print(arr[0], ' ', arr[1], ' ', arr[2], ' ', arr[3], ' ', arr[4])
end = time.time()
print('time : ', end- start)
看看两种语言实现的结果:
C库[isCpp=True]
以上两次的结果大约在0.0937s,再来看看Python的算法耗时。
Python[isCpp=False]
耗时为24s左右。
可以看到都不是一个数量级的,而且随着计算的复杂度越高。但是为啥又要用Python呢?Python简洁啊,库也多,花费少量时间写代码,运行时间可以通过并行GPU等手段优化,因此当我们提高代码运行速度,又想做一些C不好实现的逻辑代码时就可以使用C写算法,用Python实现逻辑,而且很多Python库都是使用C写的。
欢迎访问我的网站
BruceOu的哔哩哔哩
BruceOu的主页
BruceOu的博客
BruceOu的CSDN博客
BruceOu的简书