将 ctypes 函数从 Python 2 迁移到 Python 3

Migrating ctypes function from Python 2 to Python 3

如果这是一个 XY 问题,这就是我想要做的:

我有一个 wxPython 应用程序,它必须使用 WM_COPYDATA windows 消息与另一个进程通信。虽然使用 ctypes 模块发送消息出奇地简单,但接收答案需要我覆盖 wx 循环,因为 wx 没有为这种情况提供特定事件。

在 python2 上,我使用了 ctypes.windll.user32.SetWindowLongPtrWctypes.windll.user32.CallWindowProcW 函数来获得所需的行为。但是,在 python3 中,相同的代码导致 OSError: exception: access violation writing.

据我所知,python2 ctypes 模块和 python3 ctypes 模块之间的唯一区别是它们处理字符串的方式。

我也读到,这两个版本在内存布局方面存在差异,但由于我不是 C 专家,所以我无法在我的代码中找到问题。

我已经用 python3.7 (64Bit) 和 python2.7(64Bit) 和 wx 4.0.7 测试了代码(尽管它也适用于 wx2.8 和 python2)

这是最小的可重现示例:

import ctypes, ctypes.wintypes, win32con, wx, sys


_LPARAM = ctypes.wintypes.LPARAM
_WPARAM = ctypes.wintypes.WPARAM
_HWND = ctypes.wintypes.HWND
_UINT = ctypes.wintypes.UINT
_LPCWSTR = ctypes.wintypes.LPCWSTR
_LONG_PTR = ctypes.c_long
_LRESULT = _LONG_PTR
_LPCWSTR = ctypes.wintypes.LPCWSTR

_WNDPROC = ctypes.WINFUNCTYPE(_LPARAM,   # return Value
                              _HWND,     # First Param, the handle
                              _UINT,     # second Param, message id
                              _WPARAM,   # third param, additional message info (depends on message id)
                              _LPARAM,   # fourth param, additional message info (depends on message id)
)


_SetWindowLongPtrW = ctypes.windll.user32.SetWindowLongPtrW
_SetWindowLongPtrW.argtypes = (_HWND, ctypes.c_int, _WNDPROC)
_SetWindowLongPtrW.restypes = _WNDPROC

_CallWindowProc = ctypes.windll.user32.CallWindowProcW
_CallWindowProc.argtypes = (_WNDPROC, _HWND, _UINT, _WPARAM, _LPARAM)
_CallWindowProc.restypes = _LRESULT

def _WndCallback(hwnd, msg, wparam, lparam):
    print(hwnd, msg, wparam, lparam)
    return _CallWindowProc(_old_wndproc, hwnd, msg, _WPARAM(wparam), _LPARAM(lparam))
_mywndproc = _WNDPROC(_WndCallback)


app = wx.App(redirect=False)
frame = wx.Frame(None, title='Simple application')
frame.Show()

_old_wndproc = _WNDPROC( _SetWindowLongPtrW(frame.GetHandle(), win32con.GWL_WNDPROC, _mywndproc ) )
if _old_wndproc == 0:
    print( "Error" )
    sys.exit(1)

app.MainLoop()

编辑:我知道有一个 pywin32 模块,它可能对我有帮助。但是,由于代码适用于 python2,我很好奇这里发生了什么。

这里有一个问题:

_LONG_PTR = ctypes.c_long
_LRESULT = _LONG_PTR

类型LONG_PTR是“一个指针大小的整数”,它在32位和64位进程之间有所不同。由于您使用的是 64 位 Python,因此指针是 64 位的并且 LONG_PTR 应该是:

_LONG_PTR = ctypes.c_longlong

如果你想要更多的 32 位和 64 位可移植代码,LPARAM 在 Windows headers 中也被定义为 LONG_PTR 所以下面的定义会为 32 位和 64 位 Python 正确定义 LONG_PTR 因为 ctypes 已经根据 Python 的构建正确定义了它:

_LONG_PTR = ctypes.wintypes.LPARAM  # or _LPARAM in your case

更改之后,我用 wxPython 测试了您的脚本,但仍然有问题。我怀疑 wxPython 是在没有 UNICODE/_UNICODE 定义的情况下编译的,因此 SetWindowLongPtr 和 CallWindowProc API 必须使用 A 版本来检索和调用旧的 window 过程。我进行了更改并且以下代码有效。

Full code tested with 64-bit Python 3.8.8:
```py
import ctypes, ctypes.wintypes, win32con, wx, sys


_LPARAM = ctypes.wintypes.LPARAM
_WPARAM = ctypes.wintypes.WPARAM
_HWND = ctypes.wintypes.HWND
_UINT = ctypes.wintypes.UINT
_LPCWSTR = ctypes.wintypes.LPCWSTR
_LONG_PTR = _LPARAM
_LRESULT = _LONG_PTR
_LPCWSTR = ctypes.wintypes.LPCWSTR

_WNDPROC = ctypes.WINFUNCTYPE(_LRESULT,  # return Value
                              _HWND,     # First Param, the handle
                              _UINT,     # second Param, message id
                              _WPARAM,   # third param, additional message info (depends on message id)
                              _LPARAM,   # fourth param, additional message info (depends on message id)
)


_SetWindowLongPtr = ctypes.windll.user32.SetWindowLongPtrA
_SetWindowLongPtr.argtypes = (_HWND, ctypes.c_int, _WNDPROC)
_SetWindowLongPtr.restypes = _WNDPROC

_CallWindowProc = ctypes.windll.user32.CallWindowProcA
_CallWindowProc.argtypes = (_WNDPROC, _HWND, _UINT, _WPARAM, _LPARAM)
_CallWindowProc.restypes = _LRESULT

@_WNDPROC
def _WndCallback(hwnd, msg, wparam, lparam):
    print(hwnd, msg, wparam, lparam)
    return _CallWindowProc(_old_wndproc, hwnd, msg, wparam, lparam)


app = wx.App(redirect=False)
frame = wx.Frame(None, title='Simple application')
frame.Show()

_old_wndproc = _WNDPROC(_SetWindowLongPtr(frame.GetHandle(), win32con.GWL_WNDPROC, _WndCallback))
if _old_wndproc == 0:
    print( "Error" )
    sys.exit(1)

app.MainLoop()

顺便说一句,在 MSDN documentation 中有一个关于 SetWindowLongPtr(和 CallWindowProc 的类似)的注释暗示了这个解决方案:

The winuser.h header defines SetWindowLongPtr as an alias which automatically selects the ANSI or Unicode version of this function based on the definition of the UNICODE preprocessor constant. Mixing usage of the encoding-neutral alias with code that not encoding-neutral can lead to mismatches that result in compilation or runtime errors. For more information, see Conventions for Function Prototypes.