Python是目前最流行的编程语言之一。其语法简练,功能丰富,用途广泛,深受广大程序员的欢迎。
在这里我们来了解一下,Python如何在不需要第三方库的情况下,调用Win32 API。我们以“读取剪贴板中的文本”为例进行演示。我使用的操作系统是Windows 10 x64 22H2,Python是3.11.4 x64。
C版本的实现
Windows本身是用C、C++编写,所以原生支持C语言编程。要实现读取剪贴板文本的功能,C语言代码片段如下:
OpenClipboard(0);
HANDLE hg = GetClipboardData(CF_TEXT);
char *p = GlobalLock(hg);
printf("%s\n", p);
GlobalUnlock(hg);
CloseClipboard();
Python版本的实现
Python为了实现与C的底层兼容,自带了一个ctypes库,其中包括C中的数据类型和动态库调用。在Windows版本的Python中,
ctypes.windll.kernel32.GlobalLock(hg)
即可以调用该API函数。整个实现代码如下:
import ctypes as C
K = C.windll.kernel32
U = C.windll.user32
CF_TEXT = 1
U.OpenClipboard(0)
hg = U.GetClipboardData(CF_TEXT)
p = K.GlobalLock(hg)
print(C.string_at(p).decode('gbk'))
K.GlobalUnlock(hg)
U.CloseClipboard()
如果你使用的是32位Python,那么上述代码已经可以很好工作了。但是,如果是64位Python,则无法正常运行。
64位程序的问题
在64位程序中,指针、句柄、长度/尺寸等数据类型是64位的,同时又保留了32位及以下的数据类型,只不过传递、返回时将其扩展为64位,所以情况比较复杂。
而ctyps库在调用函数时,默认是没有函数原型的,除了在64位程序中显式的c_char_p、c_size_t等类型外,多数以32位int类型传递和返回,这样就导致一些API调用出现问题。
在上面的代码中,GetClipboardData函数的返回类型是HGLOBAL,其在32位系统中是32位大小,而在64位系统中是64位大小,因此64位程序中返回值是一个被截断的32位值,以这个32位值再调用下一个GlobalLock肯定出错,就算是正确的64位值,调用GlobalLock也会被截断为32位值,同样出错。GlobalUnlock亦然。
没有函数原型还导致另一个问题,就是不检查参数和返回值的类型及数量,可以传递任意类型任意数量的参数。比如:
K.GetTickCount()
K.GetTickCount(1)
都可以得到正确结果,但是在很多其它API调用时将得到错误结果或抛出异常。
那么ctypes是如何解决这个问题的呢?它采用argtypes和restype来声明函数原型。如下:
import ctypes.wintypes as W
U.GetClipboardData.argtypes = W.UINT,
U.GetClipboardData.restype = W.HGLOBAL
K.GlobalLock.argtypes = W.HGLOBAL,
K.GlobalLock.restype = W.LPVOID
K.GlobalUnlock.argtypes = W.HGLOBAL,
K.GlobalUnlock.restype = W.BOOL
请注意,W.UINT, 后面的逗号,argtypes需要一个元组(tuple)。如果argtypes或restype为空,则用None表示。其余API为偷懒可以不声明,因为它们只用到了INT/UINT类型或为空。
声明函数原型的另一个好处是可以进行类型检查和转换。
将这几行代码插入到上节代码第三行之后,现在完善后的Python程序在32位和64位环境中都可以正常运行了。