Python编程——ctypes模块

发布于 2021-09-12  412 次阅读


  此部分参考连接:聊聊Python ctypes 模块
  ctypes库是Python内建的用于调用动态链接库函数的功能模块,一定程度上可以用于Python与其他语言的回合编程。动态链接库也就是DLL文件。由于编写动态链接库,使用C/C++是最常见的方式,故ctypes最常用于Python与C/C++混合编程之中。


  因为我这边是没有看过ctypes的源码所以这边仅仅是记录一下各平台使用ctypes库所调用的函数,应该说是Python调用不同平台的动态链接库最终所使用的函数:

  Windows平台下:
    最终调用的是Windows API中LoadLibrary函数和GetProcAddress函数
  Linux和Mac OS X平台下:
    最终调用的是Posix标准中的dlopen和dlsym函数

  ctypes实现了一系列的类型转换方法,Python的数据类型会包装或直接推算为C类型,作为函数的调用参数;函数的返回值也经过一系列的包装成为Python类型。

Python和C语言混合编程

  这个作用是ctypes库最为直接的一个作用,在没有合适的编辑器的情况下,特别是遇到第三方库提供动态连接库和调用文件时,Python + ctypes是一个很好的解决方案,当然这仅仅只是在中轻量级的混合编程下。
  当然,对于某种需求,在Python自身功能能够实现的情况下,应该有限使用Python本身的功能而不要使用操作系统提供的相关API接口,这样的程序将会丧失跨平台的特性。
  这里也引用一个参考文章里的例子:

//great_module.c
#include <nmmintrin.h>

#ifdef _MSC_VER
    #define DLL_EXOIRT __declspes(dllexport)
#else
    #define DLL_EXOIRT
#endif

DLL_EXOIRT int great_function(unsigned int n){
    return _mm_popcnt_u32(n);
}

  当然,你可能看不懂这段C语言,简单解释一下,源文件中只有一个函数:great_function,他会调用Intel SSE4.2指令集中的POPCNT指令,该指令封装在_mm_popcnt_u32中,作用是计算一个无符号整数的二进制表示中“1”的个数。
  调用_mm_popcnt_u32需要包含Intel指令集的头文件nmmintrin.h,就像Python需要使用第三方库中的方法时需要import一样。
  中间的ifdef……else……endif,在MSVC下,动态链接库导出的函数必须加 __declspec( dllexport ) 进行修饰。
  然后就是C语言编译成动态链接库,因为我这边没有C语言环境,所以这边只粘贴一些命令了。

Windows MSVC 下编译命令:(启动Visual Studio命令提示)
  cl /LD great_module.c /o great_module.dll
Windows GCC、Linux、Mac OS X下编译命令相同:
  gcc -fPIC -shared -msse4.2 great_module.c -o great_module.dll

  然后写一个Python调用进行测试,最终程序可以正常运行,说明Python和C语言在ctypes的支持下可以进行混合编程。

from ctypes import *

great_module = cdll.LoadLibrary('./great_module.dll')
print(great_module.great_function(13))

# output:
# 3
# 整数13的二进制为1101,所以输出为3

类型映射:基本类型

  这部分就是书中所应用的部分了,对于数字和字符串等基本类型,ctypes采用“中间类型”的方式在Python和C之间搭建桥梁,对于C类型Tc,均有ctypes类型Tm,将其转换为Python类型Tp。简单理解一下就是C type(Tc) <--> ctypes(Tm) <--> Python type(Tp),具体来说就是某动态链接库中的函数要求参数具有C类型Tc,那么在Python ctypes 调用它的时候,就给予对应的ctypes类型Tm。Tm的值可以通过构造函数的方式传递对应的Python类型Tp,或者,使用它的可修改成员Tm.value。
  Tm(ctypes type)、Tc(C type)、Tp (Python type) 之对应关系

ctypes 类型 C 类型 Python 数据类型
c_bool _Bool bool (1)
c_char char 单字符字节串对象
c_wchar wchar_t 单字符字符串
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 __int64long long int
c_ulonglong unsigned __int64unsigned long long int
c_size_t size_t int
c_ssize_t ssize_tPy_ssize_t int
c_float float float
c_double double float
c_longdouble long doubl float
c_char_p char * (NUL terminated) 字节串对象或 None
c_wchar_p wchar_t * (NUL terminated) 字符串或 None
c_void_p void * int 或 None

  多说无益,不如来个例子,最易理解的就是print相关的例子。简单说一下C语言中的输出为printf,在C语言中printf在C标准库中,在C代码中调用是标准化的,但是,C标准库的实现不是标准化的。在Windows中,printf函数位于%SystemRoot%\System32\msvcrt.dll,在Mac OS X中,它位于/usr/lib/libc.dylib,在Linux中,一般位于/usr/lib/libc.so.6

from ctypes import *
from platfrom import *

clib = cdll.LoadLibrary(cdll_name[system()])
clib.printf(c_char_p("Helllo %d %f"),c_int(5),c_double(2.3))

  重点在最后一行,printf的原型为int printf(fonst char * format,...),这里第一个参数用c_char_p创建一个C字符串,兵役构造函数的方式用一个Python字符串进行初始化,接下来给printf一个int型和一个double型的变量,相应的,就需要将Python值转换成C类型变量,这里就用到了c_int和c_double
  当然,还有另外一种形式,使用value成员,下面代码是等价于clib.printf(c_char_p("Hello %d %f"),c_int(15),c_double(2.3)),在具体的开发环境下,不同的实现方法要按照具体情况来,不能说哪种代码量少、哪种方法简单就使用什么样的实现方法,更多的要注意程序运行的稳定性和代码的易读性。

from ctypes import *
from platform import *

str_format = c_char_p()
int_val = c_int()
double_val = c_double()

str_format.value = "Hello %d %f"
int_val.value = 15
double_val.value = 2.3
clib.printf(str_format,int_val,double_val)

  另外,一些C库函数接受指针并修改指针所指向的值。这种情况下相当于数据从C函数流回Python。仍然使用value成员获取值。

from ctypes import *
from platform import *

cdll_names = {
            'Darwin' : 'libc.dylib',
            'Linux'  : 'libc.so.6',
            'Windows': 'msvcrt.dll'
        }

clib = cdll.LoadLibrary(cdll_names[system()])
s1 = c_char_p('a')
s2 = c_char_p('b')
s3 = clib.strcat(s1,s2)
print s1.value #ab

  最后就是当ctypes可以判断类型对应关系时可以直接将Python类型赋予C函数。ctypes 会进行隐式类型转换,反之会触发异常,为了程序的稳定性,所以说要尽量少的去使用隐式类型转换。

# 隐式类型转换
s1 = c_char_p('a')
s3 = clib.strcat(s1,'b') # 等价于 s3 = clib.strcat(s1,c_char_p('b'))
print s1.value #ab

# 触发异常
clib.printf(c_char_p("Hello %d %f"),15,2.3)

高级类型映射:数组

  在C语言中,char是一种类型,char[int index]是另一种类型,对于ctypes也是一样。使用数组需要预先生成需要的数组类型。
  在之前great_module的基础上再增加一个函数array_get

//great_module.c
#ifdef _MSC_VER
    #define DLL_EXPORT __declspec( dllexport ) 
#else
    #define DLL_EXPORT
#endif 
DLL_EXPORT int array_get(int a[], int index) {
    return a[index];
}

  同样编译好dll文件后在Python程序中进行调用验证
  在Python里产生数组类型,ctypes类型承载了操作符"*",因此生成数组类型较为便捷:

from ctypes import *
great_module = cdll.LoadLibrary('./great_module.dll')

type_int_array_10 = c_int * 10

my_array = type_int_array_10()
my_array[2] = c_int(5)
print(great_module.array_get(my_array,2))

  type_int_array_10即为创建的数组类型,如果想得到数组变量,则需要例化这个类型,即my_array。my_array的每一个成员的类型应该是c_int,这里将它索引为2的成员赋予值c_int(5)。当然由于隐式转换的存在,这里写my_array[2] = 5也是可以的。
  这边有一个小思考就是有没有可能去设置一个混合数据类型的数组?
  接下来就是关于这两个小程序,函数返回值的类型,ctypes规定正式假设返回值为int,而对于array_get这个函数来说,函数返回值也是int,所以整体的程序运行是没有问题的。
  当然肯定会存在返回值不是int类型的,这就需要在调用函数之前显式的告诉ctypes返回值类型。
  举个栗子:

from ctypes import *
from platform import *

cdll_names = {
            'Darwin' : 'libc.dylib',
            'Linux'  : 'libc.so.6',
            'Windows': 'msvcrt.dll'
        }

clib = cdll.LoadLibrary(cdll_names[system()])
s3 = clib.strcat('a','b')                       # clib.strcat() C语言中用来连接字符串的函数
print s3 # an int value like 5444948
clib.strcat.restype = c_char_p                  # 显式表明返回值的类型c_char
s4 = clib.strcat('c','d')
print s4 # cd

  定义一个高维数组的方法类似,这里要注意的是C语言中并没有真正意义上的高维数组,ctypes也一样,都是利用数组的数组去实现

from ctypes import *

type_int_array_10 = c_int * 10
type_int_array_10_10 = type_int_array_10 * 10
my_array = type_int_array_10_10()
my_array[1][2] = 3

高级类型映射:简单类型指针

  ctypes 和C一样区分指针类型和指针变量。C语言中,int *是指针类型,用它声明的变量就叫指针变量,指针变量可以被赋予某个变量的地址。

  在ctypes中,指针类型用POINTER(ctypes_type)创建
  举个栗子:

type_p_int = POINTER(c_int)
v = c_int(4)
p_int = type_p_int(v)
print(p_int[0])
print(p_int.contents)

# output:
# 4
# c_long(4)

  其中,types_p_int是一个类型,这个类型是指向int的指针类型,只有将指针类型实例化后才能得到指针变量。在实例化为指针变量的同时将其指向变量v,这段代码就相当于下面的C语言代码,个人觉得还是Python比较容易理解,不过感慨一句就是C语言的指针是真的强大

typedef int * type_p_int;
int v = 4;
type_p_int p = &v;
printf("%d",p[0]);
printf("%d",*p);

  当然,由于Python是依靠绑定传递类型的语言,可以直接使用ctypes提供的pointer()得到一个变量的指针变量。

from ctypes import *

type_p_int = POINTER(c_int)
v = c_int(4)
p_int = type_p_int(v)
print(type(p_int))
print(p_int[0])
print(p_int.contents)
#-------
p_int = pointer(v)
print(type(p_int))
print(p_int[0])
print(p_int.contents)

高级类型映射:函数指针

  函数指针并没有什么特别之处。如果一个动态链接库里的某个C函数需要函数指针,那么可以遵循以下的步骤将一个Python函数包装成函数指针:

  1. 将C函数指针的原型利用ctypes的CFUNCTYPE包装成ctypes函数指针类型。
  2. 利用刚才得到的函数指针类型之构造函数,赋予其Python函数名,即得到函数指针变量。

  下面举两个栗子

第一个栗子

  第一个栗子源于ctypes的官方文档,它调用了C标准库中的qsort函数

qsort()
  原型:void qsort(void base, size_t num, size_t size, int (compar)(const void , const void))
  对base指向的数组的num个元素进行排序,每个元素大小为字节长,使用比较函数来确定顺序。此函数使用的排序算法通过调用指定的比较函数并使用指向它们的指针作为参数来比较元素对。该函数不返回任何值,但通过根据compar定义对其元素进行基数重新排序来修改指向的数组的内容。等效元素的顺序未定义。

  第三个参数即为函数指针,作为回调函数,用于给出元素之间大小的判断方法。这里使用整数作为判断类型,那么qsort的函数原型就可以理解为:

void qsort (int base, size_t num, size_t size, int (compar)(const int,const int));

  其中,回调函数的原型为:

int compar(const int,const int)

  使用CFUNCTYPE创建ctypes的函数指针类型:

CMPFUNC = CFUNCTYPE(c_int, POINTER(c_int), POINTER(c_int))

  CFUNCTYPE的第一个参数是函数的返回值,函数的其他参数紧随其后。
  接下来用Python写回调函数的实现:

def py_cmp_func(a, b):
    print(type(a))
    print("py_cmp_func", a[0], b[0])
    return a[0] - b[0]

  最后,用刚才得到的函数指针类型CMPFUNC,以Python回调函数的函数名作为构造函数的参数,就得到了可以用于C函数的函数指针变量:

p_c_cmp_func = CMPFUNC(py_cmp_func)

 完整代码:

    from ctypes import *
    from platform import *

    cdll_names = {
                'Darwin' : 'libc.dylib',
                'Linux'  : 'libc.so.6',
                'Windows': 'msvcrt.dll'
            }

    clib = cdll.LoadLibrary(cdll_names[system()])

    CMPFUNC = CFUNCTYPE(c_int, POINTER(c_int), POINTER(c_int))

    def py_cmp_func(a, b):
        print(type(a))
        print("py_cmp_func", a[0], b[0])
        return a[0] - b[0]

    type_array_5 = c_int * 5
    ia = type_array_5(5, 1, 7, 33, 99)
    clib.qsort(ia, len(ia), sizeof(c_int), CMPFUNC(py_cmp_func))

  注意到,Python函数得到的参数a和b的类型都是 POINTER(c_int) (显示为<class 'main.LP_c_int'>),对指针变量解引用的方法是之前提到的[0]或者.contents。我们这里应用了ctypes的隐式类型转换,所以a[0]和b[0]可以当成Python的int类型使用。
  这里原作者指出这段代码在*nix(Linux、Mac OS X)下调用和在Windows下调用,比较的次数是不一样的,Windows似乎更费事,这个情况我这边暂时没有验证,有兴趣的话可以自己下来验证一下。

第二个栗子

  这个栗子相对来说比较使用,但是只能在Windows下使用,因为要利用到Windows API EnumWindows枚举所有窗口的句柄,再根据窗口的句柄列出各个窗口的标题。
  感觉大佬这么说可以写外挂了啊,刚好也有在想怎么写模拟点击,之后会更一期相关的东西,看看到时候能写出来什么样的东西。
  回到正题,这里列出一个EnumWindows的文档:EnumWindows function (winuser.h)
  调用Windows API有特殊之处。由于Windows API函数不使用标准C的调用约定。故在LoadLibrary时不能够使用cdll.LoadLibrary而使用windll.LoadLibrary。在声明函数指针类型的时候,也不能用CFUNCTYPE而是用WINFUNCTYPE。关于调用约定的问题参见:x86 calling conventions
  Windows API有很多内建类型,ctypes也对应地提供了支持。栗子代码如下:

from ctypes import *
from ctypes import wintypes

WNDENUMPROC = WINFUNCTYPE(wintypes.BOOL,
                          wintypes.HWND,
                          wintypes.LPARAM)
user32 = windll.LoadLibrary('user32.dll')

def EnumWindowsProc(hwnd, lParam):
    length = user32.GetWindowTextLengthW(hwnd) + 1
    buffer = create_unicode_buffer(length)
    user32.GetWindowTextW(hwnd, buffer, length)
    print(buffer.value)
    return True

user32.EnumWindows(WNDENUMPROC(EnumWindowsProc), 0)

  这个脚本的返回值是你当前运行的所有程序的窗口名称,当然没有窗口名称的会返回空值,不过不是明确的"null",而是空白行。

写在最后

  这一篇中主要内容就是Python + C的混合编程,也就是Python的ctypes这个三方库,前半部分我想在平常的编程中也该会常用,后半部分的话算是一些高级用法吧。不过我最感兴趣的就是这个对Windows所有窗口名称的获取的代码,这个应该是作为外挂程序的一个开发,可能也会应用到一些自动化生产的脚本中,所以说下一步的话可能会结合Python的模拟点击进行相关的编写吧。