(轉)python調用dll文件
轉自https://blog.yasking.org/a/python-use-dll.html
最近接觸了一個測試,需要手動調用別人提供的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 的交互
參考: