Python2.7调用stdcall DLL可变参数函数:Win64正常Win32报错
解决32位Windows下Python 2.7调用stdcall可变参数DLL函数的栈平衡问题
这个坑我之前踩过!核心原因是32位stdcall调用约定和可变参数的冲突:stdcall要求被调用者清理栈,但可变参数函数的参数数量不确定,DLL里的这类函数实际上会退化成类似cdecl的调用逻辑(调用者负责清理栈),而Python 2.7的ctypes在处理32位stdcall函数时,默认会按被调用者清理栈的逻辑处理,导致栈不平衡,就出现了你看到的ValueError: Procedure probably called with too many arguments (4 bytes in excess)错误。
不用修改C代码的解决办法有两个,最推荐第一个:
方法一:用CFUNCTYPE(cdecl调用约定)声明可变参数函数
虽然你的DLL是stdcall,但32位下可变参数的stdcall函数实际上遵循cdecl的栈清理规则。我们可以绕过windll的默认stdcall绑定,手动用CFUNCTYPE创建函数对象:
from ctypes import * # 加载32位版本的DLL mydll = windll.LoadLibrary("mydll_32.dll") # 声明函数类型:根据你的实际函数原型调整,比如假设返回值是int,第一个参数是char* # 末尾的`...`表示支持可变参数(Python 2.7中CFUNCTYPE支持这个语法) MyVarFuncType = CFUNCTYPE(c_int, c_char_p, ...) # 从DLL中获取函数地址,创建调用对象 myvarfunc = MyVarFuncType(addressof(mydll.myvarfunc)) # 现在可以正常调用了 myvarfunc("Hello") # 如果有多个参数,比如myvarfunc("Hello %s", "World")也能正常工作
这个方法的本质是让ctypes按cdecl的规则处理栈清理,正好匹配DLL里可变参数函数的实际行为。
方法二:手动调整栈指针(不推荐,仅作备选)
如果方法一不生效,你可以在调用函数后手动修复栈指针。因为报错提示多了4字节,我们可以用ctypes调用Windows API来调整线程的栈:
from ctypes import * # 先定义线程上下文结构 class CONTEXT(Structure): _fields_ = [("ContextFlags", c_uint), ("Eax", c_uint), ("Ebx", c_uint), ("Ecx", c_uint), ("Edx", c_uint), ("Esi", c_uint), ("Edi", c_uint), ("Esp", c_uint), ("Ebp", c_uint), ("Eip", c_uint), ("EFlags", c_uint), ("SegCs", c_uint), ("SegDs", c_uint), ("SegEs", c_uint), ("SegFs", c_uint), ("SegGs", c_uint), ("SegSs", c_uint)] kernel32 = windll.kernel32 # 加载DLL并调用函数 mydll = windll.LoadLibrary("mydll_32.dll") try: mydll.myvarfunc("Hello") except ValueError: # 获取当前线程上下文 ctx = CONTEXT() ctx.ContextFlags = 0x1000 # CONTEXT_CONTROL kernel32.GetThreadContext(kernel32.GetCurrentThread(), byref(ctx)) # 调整栈指针,加上4字节(对应报错的多余字节) ctx.Esp += 4 kernel32.SetThreadContext(kernel32.GetCurrentThread(), byref(ctx))
这个方法比较hack,依赖Windows的线程上下文API,不如方法一稳定,所以优先用方法一。
另外补充下:64位下没问题是因为Windows 64位用的是fastcall调用约定,参数主要通过寄存器传递,栈平衡的逻辑和32位完全不同,ctypes可以正确处理可变参数的情况。
内容的提问来源于stack exchange,提问作者jreing
相关产品推荐
相关产品推荐

