最近接触了一个测试,需要手动调用别人提供的DLL文件,想来Python做这个事情应该是很容易,果然,网上搜索解决方案使用ctypes几行代码就可以,然而运行发现各种报错... 或者说我对DLL的了解太少了,任务让开发的同事帮忙封装成命令行执行文件,输出结果后分析文件结果搞定了,但是不琢磨一下很是不舒服...下边记录了生成DLL文件,Python调用DLL文件,还有一些注意事项,当做记录
环境: Windows7(64位)+ Python3.6(32位 / 64位)
调用都是在32位的Python下测试通过的,之后也会用64位上运行,记录一些因为Python版本不同所产生的问题
(一)DLL调用方式
主要有两种函数调用约定(__cdecl
和__stdcall
),__cdecl
是C语言默认的调用方式,__stdcall
是C++语言的标准调用方式,VC++项目默认情况下生成的是__cdecl
的,__cdecl
支持变长参数而__stdcall
方式不支持
这方面有兴趣可以自行多了解一下, 现在,知道DLL是有不同调用方式就行了
(二)Python调用msvcrt.dll
代码很简单,msvcrt.dll
是__cdecl
方式调用的DLL,代码如下
#!/bin/env python
# -*- coding: utf-8 -*-
import ctypes
lib= ctypes.CDLL('msvcrt.dll')
#lib= ctypes.cdll.LoadLibrary('msvcrt.dll')
lib.printf(b"hello world!\n")
注释部分效果等同于未注释的CDLL,类似的加载__stdcall
方式的也有两种写法
lib= ctypes.WinDLL('a.dll')
lib= ctypes.windll.LoadLibrary('a.dll')
不仅如此,通过如下这几种方式都能花式调用msvcrt.dll
# 第二种
libHandle = ctypes.windll.kernel32.LoadLibraryW('msvcrt.dll')
print(libHandle)
lib = ctypes.CDLL(None, handle=libHandle)
lib.printf(b"hello world!\n")
# 第三种
dll = ctypes.CDLL('msvcrt.dll')
lib = ctypes.CDLL(None, handle=dll._handle)
lib.printf(b"hello world!\n")
# 第四种
ctypes.cdll.msvcrt.printf(b'hello world!\n')
第二种打印了libHandle,在我电脑打印出来是这样的地址1633419264
,可以把dll文件加上绝对路径再运行一下,打印出来的值为0
,通过这个就可以知道dll是不是已经正确导入了
一般来说LoadLibrary
能够正确区分DLL的编码类型,或者显示的进行调用,LoadLibraryW
用来打开Unicode
编码的DLL,LoadLibraryA
用来打开ANSI
编码的DLL
然后再使用CDLL指定handle加载DLL,就可以使用了
第三种没有用windos的API加载DLL,而是直接使用CDLL加载,这个返回的对象中的_handle
属性才是我们要的handle,再次使用CDLL加载就可以使用了
第四种是msvcrt
支持的一种方式,不用显式load
尝试将第二种代码修改些东西,看会报什么错
1)将LoadLibraryW
修改为LoadLibraryA
Traceback (most recent call last):
File ".\cmp.py", line 14, in <module>
lib.printf(b"hello world!\n")
File "C:\ProgramData\Anaconda3\lib\ctypes\__init__.py", line 357, in __getattr__
func = self.__getitem__(name)
File "C:\ProgramData\Anaconda3\lib\ctypes\__init__.py", line 362, in __getitem__
func = self._FuncPtr((name_or_ordinal, self))
AttributeError: function 'printf' not found
没有按照正确的编码方式打开,不能从DLL中获取函数
2)将CDLL
修改位WinDLL
Traceback (most recent call last):
File ".\cmp.py", line 14, in <module>
lib.printf(b"hello world!\n")
ValueError: Procedure probably called with too many arguments (4 bytes in excess)
DLL成功加载后,因为使用了错误的调用方式,所以参数这个发生了错误
所以调试的时候分两步,一是
(三)制作一个DLL
我用的是Code::Blocks,在Code::Blocks创建工程,选择Dynamic Link Library
,创建后会有两个文件,一个cpp一个h文件,将两个文件清空,cpp文件中写入如下代码:
1)__stdcall
调用方式
//main.cpp
#define DLLEXPORT extern "C" __declspec(dllexport)
DLLEXPORT int __stdcall sum(int a, int b) {
return a + b;
}
2)__cdecl
调用方式
//main.cpp
#define DLLEXPORT extern "C" __declspec(dllexport)
DLLEXPORT int __cdecl sum(int a, int b) {
return a + b;
}
我没在h文件中写声明,似乎是可有可无的。点击编译,在项目的bin\Debug\
目录就能找到编译好的DLL文件了
使用最简单的方式调用
lib = ctypes.CDLL('cdecl_sum.dll')
a = lib.sum(1, 2)
print(a)
lib2 = ctypes.WinDLL('stdll_sum.dll')
summmm = getattr(lib2, 'sum@8')
a = summmm(3, 4)
print(a)
上边导出的__stdcall
调用方式的DLL函数名称变为了sum@8
,这个是__stdcall
的导出函数的命名规则,可以使用getattr来获取
作为辅助,可以使用Dependency Walker查看DLL中函数名
下载:http://www.dependencywalker.com/
用这个工具打开DLL,就可以看到DLL中导出的文件名称
(四)Python 64位运行的问题
Python 64位加载32位的DLL,使用第二种方式,加载DLL返回值是0
,输出如下,很难定位问题
Traceback (most recent call last):
File ".\cmp.py", line 31, in <module>
a = lib.sum(1, 2)
File "C:\ProgramData\Anaconda3\lib\ctypes\__init__.py", line 357, in __getattr__
func = self.__getitem__(name)
File "C:\ProgramData\Anaconda3\lib\ctypes\__init__.py", line 362, in __getitem__
func = self._FuncPtr((name_or_ordinal, self))
AttributeError: function 'sum' not found
使用CDLL那个最简单的方式调用
Traceback (most recent call last):
File ".\cmp.py", line 45, in <module>
lib = ctypes.CDLL('cdecl_sum.dll')
File "C:\ProgramData\Anaconda3\lib\ctypes\__init__.py", line 344, in __init__
self._handle = _dlopen(self._name, mode)
OSError: [WinError 193] %1 不是有效的 Win32 应用程序。
这个提示就挺明显了,是64与32位导致的不识别问题
先留一点坑,有时间生成64位的DLL测试一下,就不会有问题了,先这样
【2017-06-28】补充
关于调用传参和返回值的问题
需要进行手动指定,否则载入成功dll,调用会失败,返回奇奇怪怪的数字
dll.addf.restype = c_float
dll.addf.argtypes = (c_float, c_float)
具体的参看这篇博文,说的挺详细的:python ctypes 探究 ---- python 与 c 的交互
参考: