Python调用DLL文件

Published: 2017-06-20

Tags: Python


最近接触了一个测试,需要手动调用别人提供的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 的交互

参考:

  1. 函数调用规约(__stdcall 和 __cdecl 的区别浅析)
  2. The Python Standard Library: ctypes
  3. How do I compile for 64bit using G++ w/ CodeBlocks?