• python调用C/C++动态库


    本文以windows环境下的.dll动态链接库为背景展开,有关linux下的.so动态链接库的相关用法会在另外一篇文章中展开讲解。

    1. 背景知识

    一直以来python都被称为胶水语言,能够轻易地操作其他程序,轻易地包装使用其他语言编写的库。下面简单介绍下如何使用python来调用C/C++编写的动态库。首先了解下动态链接库及C/C++动态库的区别。

    1.1 动态链接库

    使用VS2017创建动态链接库DllDemo,代码如下:

    //整型相加
     __declspec(dllexport) int addInt(int a, int b) 
    {
    	return a + b;
    }
    //浮点型相加
    __declspec(dllexport) float addFloat(float a, float b) 
    {
    	return a + b;
    }
    //传递float指针类型参数
    __declspec(dllexport) void changeFloat(float *a) 
    {
    	*a = 100.00;
    }
    //传递char指针类型参数,返回指针类型参数
    __declspec(dllexport) char* pointCh(char *a) 
    {
    	return a;
    }
    

    使用dumpbin.exe工具,在cmd命令行使用

    dumpbin.exe -exports DllDemo.dll
    

    查看动态库的导出函数如下:

    从上图可以看出动态库导出了4个函数,与上面代码中的导出函数一致。但是函数名长得很奇怪,这是因为在编译链接时,C++会按照自己的规则篡改函数的名称,这一过程称为"名字改编",C++支持函数重载,就是在函数名字改编阶段记录下函数的相关参数信息。

    C++标准并没有定义名字改编的标准,因此会导致不同编译器编译出来的动态库不能通用。

    1.2 extern "C"作用

    相比之下,C标准规定了名字改编的标准,extern "C"指示编译器在编译代码是按照C的标准进行编译。
    在上述代码的每个函数名前都加上extern "C"关键字。

    extern "C" __declspec(dllexport) int addInt(int a, int b) 
    {
    	return a + b;
    }
    
    extern "C" __declspec(dllexport) float addFloat(float a, float b) 
    {
    	return a + b;
    }
    
    extern "C" __declspec(dllexport) void changeFloat(float *a) 
    {
    	*a = 100.00;
    }
    
    extern "C" __declspec(dllexport) char* pointCh(char *a) 
    {
    	return a;
    }
    

    查看动态库的导出函数,可以看到导出的函数名和函数的定义是一致的。

    如果extern "C"修饰的函数进行了重载,则会在编译时报错,因为C语言并不支持函数的重载。

    1.3 动态链接库加载方式

    1.3.1 隐式链接

    隐式链接需要.dll .lib文件

    #include <iostream>
    #include <Windows.h>
    using namespace std;
    
    //隐式加载需要导入动态库的导入库
    #pragma comment(lib, "../Debug/DllDemo.lib")
    
    //dll中导出的函数通过直接声明或者包含头文件的方式
    extern "C" _declspec(dllimport) int addInt(int a, int b);
    extern "C" _declspec(dllimport) float addFloat(float a, float b);
    
    int main()
    {
        //调用动态库中的函数
    	cout << addInt(1, 2) << endl;
    	cout << addFloat(1.25, 2.25) << endl;
    }
    

    通过vs自带的工具lib,通过命令lib /list XXXX.lib,可以查看一个.lib文件是静态库还是导入库

    1.3.2 显式链接

    显式链接需要.dll, 需要事先知道导出函数的签名,不需要.lib和.h文件

    #include <iostream>
    #include <Windows.h>
    using namespace std;
    
    //声明导出函数的类型
    typedef int(*addInt)(int a, int b);
    typedef float(*addFloat)(float a, float b);
    
    int main()
    {
    	HINSTANCE hDLL;
    
    	//定义导出函数
    	addInt addInt_;
    	addFloat addFloat_;
    	
    	//加载动态库,开辟内存
    	hDLL = LoadLibrary("./DllDemo.dll");
    	if (hDLL != NULL)
    	{
    		//获取导出函数
    		addInt_ = (addInt)GetProcAddress(hDLL, "addInt");
    		if (addInt_)
    		{
    			cout << addInt_(1, 2) << endl;
    		}
    
    		addFloat_ = (addFloat)GetProcAddress(hDLL, "addFloat");
    		if (addFloat_)
    		{
    			cout << addFloat_(1.25, 2.25) << endl;
    		}
    		//卸载动态库,释放内存
    		FreeLibrary(hDLL);
    	}
    
    	return 0;
    }
    

    使用显式加载的好处:

    • 通过显式加载动态库的方式可以根据需要加载相应的函数,随时可以卸载,通过判断句柄的方式,不会因为找不到dll导致程序无法启动。
    • 如果程序需要访问十多个dll,采用动态加载的方式可以减少程序的启动时间,减小程序占用的内存

    2. python操作动态库

    python操作动态库是通过ctypes这个内建的包,官方文档ctypes。因为上述C++动态库“名字改编”的问题,导致直接使用C++代码中函数名字时无法调用,必须使用经过名字改编之后的函数名,在使用C++编译的动态链接库时,最好使用extern "C"来辅助,可以通过对C++动态库进行简单的封装转换为C动态库,这样可以在使用时直接调用动态库中定义的函数即可,不需要考虑函数“名字改编”的问题。

    先上代码:

    #--*--utf8--*--
    from ctypes import *
    
    # 加载动态库
    Objdll = cdll.LoadLibrary('DllDemo.dll')
    
    def dllTest():
        # python调用动态库默认参数为整型
        print(Objdll.addInt(10, 20))
        
        # 直接调用addFloat接口
        print(Objdll.addFloat(10, 2))
    
        # 设置动态库函数的参数和返回值类型为float
        Objdll.addFloat.restype = c_float
        Objdll.addFloat.argtypes = (c_float, c_float)
        print(Objdll.addFloat(10.0, 20.0))
    
        # 设置动态库的参数类型float*
        infloat = c_float(1.0)
        Objdll.changeFloat.restype = None
        Objdll.changeFloat.argtypes = (POINTER(c_float), )
        Objdll.changeFloat(byref(infloat))
        print(infloat.value)
    
        # 设置动态库的参数类型为char*
        pInCh = bytes('Hello World', 'utf-8')
        Objdll.pointCh.restype = c_char_p
        Objdll.pointCh.argtypes = (c_char_p, )
        pOutCh = Objdll.pointCh(pInCh)
        print(pOutCh.decode('utf-8'))
    
    if __name__ == "__main__":
        dllTest()
    

    代码输出:

    2.1 加载动态库

    Objdll = cdll.LoadLibrary('DllDemo.dll')

    2.2 指定函数命名

    查看在上图中直接调用addFloat的代码输出

        # 直接调用addFloat接口
        print(Objdll.addFloat(10, 2))
    

    发现输出的结果并不是预期的12.0而是一个特别大的数字,这是因为python在调用动态库的函数时,如果不进行指定,则默认的参数类型和返回值类型均为整型,且调用时不会像在C/C++中一样进行自动的类型转换。python在调用动态库中的函数时需要指定函数的参数类型和返回值类型。

    通过Objdll._FuncPtr.restype来指定动态库函数的返回值类型,通过Objdll._FuncPtr.argtypes来指定动态库函数的参数类型,Objdll._FuncPtr.argtypes的类型为turple,包含动态库函数的参数类型列表,指定的参数类型必须为C/C++中参数类型所对应的ctypes类型。

    2.3 参数类型

    python类型和C语言类型的对应关系:

    ctypes type C type python type
    c_bool _Bool bool (1)
    c_char char 1-character bytes object
    c_wchar wchar_t 1-character string
    c_byte char int
    c_ubyte unsigned char int
    c_short short int
    c_ushort unsigned short int
    c_int int int
    c_uint unsigned int int
    c_long long int
    c_ulong unsigned long int
    c_longlong __int64 or long long int
    c_ulonglong unsigned __int64 or unsigned long long int
    c_size_t size_t int
    c_ssize_t ssize_t or Py_ssize_t int
    c_float float float
    c_double double float
    c_longdouble long double float
    c_char_p char * (NUL terminated) bytes object or None
    c_wchar_p wchar_t * (NUL terminated) string or None
    c_void_p void * int or None

    2.3.1 值类型

    对于参数类型和返回值类型都为值类型的动态库函数,操作相对简单,只需要指定对应的参数和返回值ctype类型即可进行调用。

    2.3.2 指针类型

    创建ctypes类型的指针需要借助三个相关的函数:

    函数 说明
    byref(x[,offset]) 返回x的地址,相当于C的&x。可选参数offset表示偏移量
    pointer(x) 创建并返回一个指向x的指针实例,x是一个实例对象
    POINTER(x) 返回一个类型,这个类型是指向ctypes类型的指针类型

    byref相当于C的取地址符号,在参数传递时可以通过byref传递函数的指针。pointerPOINTER的区别是,pointer返回的是一个实例,而POINTER返回的是一个类型。

    动态库函数原型:

    extern "C" __declspec(dllexport) void changeFloat(float *a) 
    {
    	*a = 100.00;
    }
    

    python中调用:

    # 加载动态库
    Objdll = cdll.LoadLibrary('DllDemo.dll')
    
    # 定义函数参数
    infloat = c_float(1.0)
    
    # 指定函数返回值类型为void
    Objdll.changeFloat.restype = None
    
    # 指定函数的参数类型为float*
    Objdll.changeFloat.argtypes = (POINTER(c_float), )
    
    # 调用函数,通过byref函数将参数包装成float*类型
    Objdll.changeFloat(byref(infloat))
    
    # 打印结果
    print(infloat.value)
    

    2.3.3 引用类型

    C语言中没有引用类型,如果动态库是由C++编写存在引用类型参数的函数,需要先用指针类型包装成C动态库。

    2.3.4 结构体类型

    动态库函数定义:

    typedef struct _SimpleStruce
    {
    	int nNo;
    	float fVirus;
    	char szBuffer[512];
    }SimpleStruct, *PSimpleStruct;
    
    extern "C" __declspec(dllexport) int PrintStruct(PSimpleStruct simp)
    {
    	cout << "nNo: " << simp->nNo << ", fVirus: " << simp->fVirus << ", szBuffer: " << simp->szBuffer << endl;
    	return simp->nNo;
    }
    

    python中调用

    # 定义class和动态库中的类型相对应,成员变量名必须一致
    class SimpStruct(Structure):
        _fields_ = [("nNo", c_int), ("fVirus", c_float), ("szBuffer", c_char*512)] 
    
    # 定义变量,并对各个成员变量名进行赋值
    simpStruct = SimpStruct()
    simpStruct.nNo = 16
    simpStruct.fVirus = 3.14
    simpStruct.szBuffer = bytes('Hello World','utf-8')
    
    # 定义函数的参数和返回值
    Objdll.PrintStruct.restype = c_int
    Objdll.PrintStruct.argtypes = (POINTER(SimpStruct),)
    
    # 调用动态库函数,传递结构体指针
    resnNo = Objdll.PrintStruct(byref(simpStruct))
    
    # 打印动态库函数返回值
    print(resnNo)
    

    python调用结果:

    nNo: 16, fVirus: 3.14, szBuffer: Hello World
    16
    

    3.总结

    总结起来,在python中调用dll中方法的一般步骤为:

    1. 使用extern c关键字对dll进行包装。
    2. 使用ctypes库加载动态库。
    3. 根据C中类型和ctypes中类型的对应关系指定动态库函数的返回值类型和参数类型。
    4. 调用动态库中的函数。
    5. 对函数的返回结果进行转换成python中的类型进行使用。
  • 相关阅读:
    windows mobile 上文件压缩与解压缩之二
    windows mobile多线程示例
    JDK 1.5 环境变量的配置
    PDA连接远程数据库的三种解决方案(转)
    Windows Mobile连接数据库的几种方式(转)
    .Net Compact Framework 调用 Java WebService
    MyEclipse 中文乱码
    Tomcat环境变量配置
    XML on Windows Mobile (C#)
    从VS2005项目转换为VS2008项目(C#版)
  • 原文地址:https://www.cnblogs.com/BlueskyRedsea/p/10327809.html
Copyright © 2020-2023  润新知