从枚举Windows系统进程看python ctypes与C的Win32混和编程

环境:python2.7

下面是枚举Windows所有进程,根据已知进程名获取进程信息(pid)的实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def getPidByName(processName):
ret = []
aProcesses = (wintypes.DWORD * 1024)()
cbNeeded = wintypes.DWORD(0)

if not ctypes.windll.psapi.EnumProcesses(aProcesses, ctypes.sizeof(aProcesses), ctypes.byref(cbNeeded)):
return ret

cProcesses = cbNeeded.value / ctypes.sizeof(wintypes.DWORD)
for i in range(cProcesses):
if aProcesses[i] != 0:
szProcessName = ctypes.create_unicode_buffer(ctypes.sizeof(wintypes.WCHAR) * wintypes.MAX_PATH)
hProcess = ctypes.windll.kernel32.OpenProcess(0x0400 | 0x0010, False, aProcesses[i])
if hProcess != None:
ctypes.windll.psapi.GetModuleBaseNameW(hProcess, None, szProcessName, ctypes.sizeof(szProcessName) / ctypes.sizeof(wintypes.WCHAR))
if str(szProcessName.value) == processName:
ret.append(int(aProcesses[i]))
ctypes.windll.kernel32.CloseHandle(hProcess)
return ret

基本数据类型

了解一种编程语言的数据类型是通往这条编程大道的基本入门法则,下面是ctypes中python与C对应类型表:
|ctypes type|C type|Python type|
| :———| :—-| :———|
| c_bool | _Bool | bool (1) |
| c_char | char | 1-character string |
| c_wchar | wchar_t | 1-character unicode string |
| c_byte | char | int/long |
| c_ubyte | unsigned char | int/long |
| c_short | short | int/long |
| c_ushort | unsigned short | int/long |
| c_int | int | int/long |
| c_uint | unsigned int | int/long |
| c_long | long | int/long |
| c_ulong | unsigned long | int/long |
| c_longlong | __int64 or long long | int/long |
| c_ulonglong | unsigned __int64 or unsigned long long | int/long |
| c_float | float | float |
| c_double | double | float |
| c_longdouble | long double | float |
| c_char_p | char * (NUL terminated) | string or None |
| c_wchar_p | wchar_t * (NUL terminated) | unicode or None |
| c_void_p | void * | int/long or None |

先简单看下基本类型说明:

  1. 初始化及构造
    基本类型都可以通过类似于C构造函数的方式来声明和初始化。
    比如要声明一个c_bool, cb = c_bool(True)

  2. None 对应C中的 NULL
    NULL在win32编程中经常用到,所以这一条还是很有用的。实际上,用0也能表示,毕竟在Windows中NULL是0用宏来实现的。

  3. c_char_p、 c_wchar_p、 c_void_p 构建出的指针,其内容是不可变的,赋新值时实际上是指向了新地址。官方的例子很生动,引用下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    >>> s = "Hello, World"
    >>> c_s = c_char_p(s)
    >>> print c_s
    c_char_p('Hello, World')
    >>> c_s.value = "Hi, there"
    >>> print c_s
    c_char_p('Hi, there')
    >>> print s # first string is unchanged
    Hello, World
    >>>

可以看出s最终是没有被修改的。也证实了一点,Python中string类型是不可变(immutable)的。

  1. 创建可变的string buffer。
    Win32编程中经常要用到字符数组。比如传递空的字符数组到某API中,获取信息并保存到数组中。那么这里肯定需要可变的string buffer了。
    ctypes中可使用create_string_buffer()create_unicode_buffer()两个方法。
    其中,create_string_buffer()生成c_char类型的字符数组,而create_unicode_buffer()生成c_wchar类型的。
普通数组

沿用最上面的枚举进程的代码,这个进程id数组声明如下:
aProcesses = (wintypes.DWORD * 1024)()
可以看出,基本格式是: (类型 * 大小)()
数组可以直接作为指针传递

指针与引用

Python ctypes中
指针: pointer()
引用: byref()
指针相当于C中的左值, 本身可以被修改,被操作(取地址):

1
2
3
4
5
6
7
>>> from ctypes import *
>>> n = c_int(0)
>>> p = pointer(n)
>>> p1 = POINTER(c_int)
>>> pp = byref(p)
>>> type(pp)
<type 'CArgObject'>

注意:pointer(t)相当于POINTER(type(t)), 也就是说可以用POINTER()来生成(可以理解为注册)新指针类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
>>> from ctypes import *
>>> new_p_type = POINTER(c_int * 10)
>>> type(new_p_type)
<type '_ctypes.PyCPointerType'>
>>> n = new_p_type((c_int * 10)(0))
>>> type(n)
<class '__main__.LP_c_long_Array_10'>
>>> new_p1_type = POINTER(c_int)
>>> type(new_p1_type)
<type '_ctypes.PyCPointerType'>
>>> n1 = new_p1_type(c_int(0))
>>> type(n1)
<class '__main__.LP_c_long'>
```
而引用相当于C中的右值,本身没有被分配空间:
```python
>>> from ctypes import *
>>> n = c_int(0)
>>> f = byref(n)
>>> ff = byref(f)
Traceback (most recent call last):
File "<input>", line 1, in <module>
TypeError: byref() argument must be a ctypes instance, not 'CArgObject'

在Win32编程中,一般有两种情况需要用到ctypes的指针和引用。
一种是普通的一维指针传递,这时使用byref()即可。还一种是多维指针,这里就需要用到pointer()构建指针对象,最后使用byref()传递。
例如**T这种二维指针。

类型转换

使用cast()来进行类型转换(强转)。
例如:

1
2
3
4
5
6
7
>>> from ctypes import *
>>> n = pointer(c_ubyte(1))
>>> n1 = cast(n, POINTER(c_uint))
>>> n
<__main__.LP_c_ubyte object at 0x05D866C0>
>>> n1
<__main__.LP_c_ulong object at 0x053A5850>

DLL加载

说到Win32编程,肯定是需要调用各种DLL的,比如系统库,C运行库等。
用法比较简单,两种方式:

  1. ctypes.LibraryLoader.LoadLibrary(dllname)
    调用该dll的方法较多时,可先保存module再直接使用变量调用。自定义dll使用此方式,dllname即为dll路径
  2. ctypes.LibraryLoader.dllname
    在调用方法较少的情况下使用比较方便。

注意:如果dll不存在,C/C++下加载dll可以判断句柄是否为空,但这里两种方式都会抛异常。可以使用find_library(name)先查找,如果存在时再加载。

回调函数

回调机制在Win32编程中十分重要,很多API都需要传入一个回调函数作为参数。
在ctypes中,有几种声明回调函数的方式。我们来看看它们有什么区别。

  1. CFUNCTYPE(restype, *argtypes, use_errno=False, use_last_error=False)
    一般C回调函数,__stdcall的调用方式。
  2. WINFUNCTYPE(restype, *argtypes, use_errno=False, use_last_error=False)
    Window API专用回调函数,和C中WINAPI这个宏有异曲同工之妙。自然是__cdecl的调用方式了。
  3. PYFUNCTYPE(restype, *argtypes)
    Python回调函数,混和编程中较少使用。

扩展:__cdecl__stdcall有什么区别呢?

__stdcall:参数由右向左压入堆栈;堆栈由函数本身清理。

__cdecl:参数也是由右向左压入堆栈;但堆栈由调用者清理。

两者在同一名字修饰约定下,编译过后变量和函数的名字也不一样。