Autoahotkey關(guān)于dllcall()函數(shù)最全面的解釋

這是一篇很長的技術(shù)文章,需要極強的耐心才能讀完,如果想理解得讀兩邊!

DllCall是AHK的一個強大功能,用來調(diào)用Dll****文件中的函數(shù)。

用法格式如下:

Result := DllCall("DllFile\Function" , Type1, Arg1, Type2, Arg2, "Cdecl ReturnType")

許多新手一看這一長串,倒吸一口冷氣,這是啥玩意!可能覺得DllCall很復(fù)雜、很難用,于是對Windows強大的WinAPI函數(shù)就不敢上手,只能羨慕那些大神們調(diào)用WinAPI實現(xiàn)各種奇妙功能。其實DllCall的使用并不難,下面我就為大家撥開DllCall的神秘面紗,其實你也能簡單學(xué)會!


驚嚇_small.jpg

一、dll是啥?

DLL(Dynamic Link Library)文件為動態(tài)鏈接庫文件,又稱“應(yīng)用程序拓展”,是軟件文件類型。在Windows中,許多應(yīng)用程序并不是一個完整的可執(zhí)行文件,它們被分割成一些相對獨立的動態(tài)鏈接庫,即DLL文件,放置于系統(tǒng)中。當(dāng)我們執(zhí)行某一個程序時,相應(yīng)的DLL文件就會被調(diào)用。一個應(yīng)用程序可使用多個DLL文件,一個DLL文件也可能被不同的應(yīng)用程序使用,這樣的DLL文件被稱為共享DLL文件。

DLL文件中存放的是各類程序的函數(shù)(子過程)實現(xiàn)過程,當(dāng)程序需要調(diào)用函數(shù)時需要先載入DLL,然后取得函數(shù)的地址,最后進行調(diào)用。使用DLL文件的好處是程序不需要在運行之初加載所有代碼,只有在程序需要某個函數(shù)的時候才從DLL中取出。另外,使用DLL文件還可以減小程序的體積。

組成一個軟件的文件中.dll占據(jù)相當(dāng)多一部分。

通過使用 DLL,程序可以實現(xiàn)模塊化,由相對獨立的組件組成。 例如,一個計帳程序可以按模塊來銷售。 可以在運行時將各個模塊加載到主程序中(如果安裝了相應(yīng)模塊)。 因為模塊是彼此獨立的,所以程序的加載速度更快,而且模塊只在相應(yīng)的功能被請求時才加載。

此外,可以更為容易地將更新應(yīng)用于各個模塊,而不會影響該程序的其他部分。 例如,您可能具有一個工資計算程序,而稅率每年都會更改。 當(dāng)這些更改被隔離到 DLL 中以后,您無需重新生成或安裝整個程序就可以應(yīng)用更新。

(下面都以WinApi函數(shù)為例)

我們都用過AHK的內(nèi)置函數(shù)或自己寫的函數(shù),比如:Pos:=InStr("abc123", "abc")

假如一個Dll文件中也有一個InStr函數(shù),而且那個函數(shù)也需要兩個字符串參數(shù),怎么調(diào)用呢?

調(diào)用的格式為:

Pos:=DllCall("Dll文件\InStr", "Str","abc123", "Str","abc", "Int")

我們比較一下區(qū)別:AHK函數(shù)的函數(shù)名InStr,相當(dāng)于DllCall調(diào)用中的DllCall("Dll文件\InStr"部分;AHK函數(shù)的兩個參數(shù),在DllCall調(diào)用中每個參數(shù)前面都加了個說明參數(shù)類型的參數(shù);最后DllCall調(diào)用的尾部還多出了一個說明函數(shù)返回類型的參數(shù)。

函數(shù)名部分很容易照搬使用,函數(shù)返回類型也只有兩三種比較容易,只有參數(shù)類型有點難度。

要學(xué)會并自由使用DllCall需要進修三步:

  • 第一步要學(xué)會準(zhǔn)確設(shè)定參數(shù)類型。(了解&為取變量的內(nèi)存首地址的操作符)
  • 第二步要學(xué)會用VarSetCapacity()分配內(nèi)存,及用NumGet()、NumPut()讀寫內(nèi)存數(shù)據(jù)。
  • 第三步要學(xué)會用StrGet()、StrPut()轉(zhuǎn)換字符串編碼。

利用StrPut可以將原生編碼(由AHK是ANSI版還是Unicode版決定的)的字符串轉(zhuǎn)換為目標(biāo)編碼,StrGet讀取目標(biāo)編碼轉(zhuǎn)換為當(dāng)前的原生編碼。

第二、三步看幫助文件就很容易懂了,第一步我慢慢講解,引導(dǎo)大家深入理解參數(shù)類型。

AHK普通函數(shù)的區(qū)別在于每個參數(shù)要根據(jù)調(diào)用函數(shù)的聲明添加對應(yīng)的參數(shù)類型和設(shè)定函數(shù)返回類型(這兩個是重點)。

二、為什么每個參數(shù)前面都要設(shè)定準(zhǔn)確的參數(shù)類型?

1、DllCall調(diào)用的大概流程是:DllCall首先把Dll文件整個讀取到AHK進程的私有內(nèi)存中,然后通過函數(shù)名字符串找到對應(yīng)的函數(shù)入口地址,然后把參數(shù)一個一個壓入AHK進程的棧中,然后跳轉(zhuǎn)到函數(shù)的入口地址,控制權(quán)交給了入口地址的機器碼手中。機器碼會從棧中把參數(shù)的數(shù)值一個一個讀取回來,然后執(zhí)行自身的代碼。機器碼執(zhí)行完畢后,會把返回值寫入通用寄存器EAX中,然后把控制權(quán)歸還給AHK,然后AHK從通用寄存器EAX中讀取返回值。

2、我們知道,AHK普通函數(shù)是動態(tài)解析的,所以各個函數(shù)參數(shù)都用逗號作為分隔符隔開,不用考慮參數(shù)的數(shù)據(jù)類型(實際上AHK把各種變量包括數(shù)字都保存為字符串類型),而調(diào)用Dll中的函數(shù),要把參數(shù)的數(shù)值連續(xù)壓入棧中,中間可是沒有分隔符的,各參數(shù)在棧中所占的字節(jié)數(shù)也可能不同,這就很容易搞混。所以必須約定好每個參數(shù)占據(jù)幾個字節(jié),以函數(shù)聲明的約定來區(qū)分各個參數(shù)。

我要特別強調(diào)的是,所有傳給機器碼的各個參數(shù)都是一個數(shù)值,字符串是把字符串在內(nèi)存中的首地址壓入棧中,數(shù)據(jù)結(jié)構(gòu)也是把內(nèi)存首地址壓入棧中,簡單數(shù)字和地址值就更是數(shù)值了。如果所有機器碼對應(yīng)的WinAPI函數(shù),參數(shù)都用固定的數(shù)值類型,比如4字節(jié)的整型Int來接收,那么這樣約定后,AHK傳遞參數(shù)時就不需要說明參數(shù)類型了,每個參數(shù)都壓入棧中占用4個字節(jié)就好了。但是WinAPI的參數(shù)很多都是內(nèi)存地址,比如字符串是字符串的內(nèi)存首地址,數(shù)據(jù)結(jié)構(gòu)(比如常見的句柄)也是內(nèi)存首地址等等,而內(nèi)存地址在Win32位系統(tǒng)中是占4字節(jié),在Win64位系統(tǒng)中是占8字節(jié)。WinAPI的參數(shù)也有一些是簡單數(shù)值,比如長、寬、顏色值等等,假如無腦全部約定Win32位系統(tǒng)的參數(shù)都占4字節(jié),Win64系統(tǒng)的參數(shù)都占8字節(jié),確實方便了其他人調(diào)用,但是卻浪費了棧的空間,而且如果Win32位系統(tǒng)需要調(diào)用或者返回一個8字節(jié)的整型Int64參數(shù),那么也會打破這種無腦的約定。所以編寫WinAPI函數(shù)的程序員不會遷就調(diào)用者來個死板約定,該用地址型就地址型(這在AHK中對應(yīng)Ptr類型,會根據(jù)AHK自身是32位版還是64位版自動為4字節(jié)或8字節(jié)),該用整型就整型(這在AHK中對應(yīng)Int占用4字節(jié)),那么調(diào)用者就要遷就函數(shù)編寫者的聲明了,函數(shù)編寫者發(fā)布函數(shù)時,都會聲明每個參數(shù)的數(shù)據(jù)類型(表面上五花八門,實際上基本上就是地址型和整型兩種),所以DllCall調(diào)用時,就要按照WinAPI函數(shù)的聲明,在每個參數(shù)前面加上合適的參數(shù)類型,參數(shù)的數(shù)值壓入棧中時,該占8字節(jié)就占8字節(jié),該占4字節(jié)就占4字節(jié),這樣WinAPI函數(shù)讀取的時候根據(jù)函數(shù)聲明來讀取就不會出錯了。(如果參數(shù)類型錯誤,機器碼讀到非法的值會讓AHK程序崩潰)

注:為了節(jié)約棧的空間,Api設(shè)定每個參數(shù)時,不都是占用4字節(jié)(32位操作系統(tǒng)的地址指針大?。┗?字節(jié)(64位操作系統(tǒng)的地址指針大?。?,而是Char類型1字節(jié),Short類型2字節(jié),Int和Float類型4字節(jié),Int64和Double類型8字節(jié),Ptr類型(即地址指針類型)視操作系統(tǒng)為32位還是64位自動為4字節(jié)或8字節(jié)(AHK提供此類型可以自適應(yīng)操作系統(tǒng)的地址指針大?。?。

Api函數(shù)運行時,把棧上的各參數(shù)按棧頂加偏移(字節(jié)數(shù))來讀取各個數(shù)據(jù),AHK如果不按Api函數(shù)聲明的約定字節(jié)數(shù)來傳遞各個參數(shù),如果有一個參數(shù)的類型錯誤(字節(jié)數(shù)不對),Api讀取后面參數(shù)的偏移就會出錯。

三、****AHK****參數(shù)類型的分類說明:(以調(diào)用****WinApi****函數(shù)為代表)

1、傳遞給WinApi的參數(shù)其實只有兩類:簡單數(shù)值和內(nèi)存地址。前者包含:int、int64、float、double等,后者包含:char(charp同義)、ptr、str三種。一般與WinApi函數(shù)的聲明格式對應(yīng),也可以不一致但字節(jié)數(shù)一致就行了。比如char、ptr、str類型都是傳址,AHK會智能根據(jù)自身是32位版的還是64位版的將對應(yīng)的值壓入棧中占4字節(jié)或8字節(jié)。如果我們已經(jīng)知道AHK是32位版的,用int代替ptr也是可以的,因為int也是把數(shù)值壓入棧中占4字節(jié)。只要確保每個參數(shù)的字節(jié)數(shù)匹配WinApi函數(shù)聲明的約定字節(jié)數(shù)就行。

2、如果WinApi需要傳入字符串怎么辦呢?沒什么問題,字符串在內(nèi)存中是連續(xù)的編碼數(shù)值,只要傳入它的起始地址就行了,如果有另外的參數(shù)傳入了長度,則可以準(zhǔn)確處理這個長度的字符串,如果沒有另外傳入長度,則以字符串的默認結(jié)束符來處理字符串(Ansi編碼的字符串以1個字節(jié)值0結(jié)束,Unicode編碼的字符串以2個字節(jié)值0結(jié)束)。AHK為輸入字符串設(shè)置了str類型,后面的參數(shù)可以是原義的字符串比如"OK",AHK會自動把這個字符串保存到臨時變量a,然后把變量的地址(&a)壓入棧中。如果后面的值是一個變量b,則省了臨時變量,直接把地址(&b)壓入棧中,此時它的輸入功能與ptr,&b是一樣的。但是與ptr形式的區(qū)別是它可以更新后面的變量(輸出功能不同),即更新字符串長度 VarSetCapacity(b,-1)。因為AHK內(nèi)部對大多數(shù)變量都視為字符串,并標(biāo)記了變量長度,AHK對變量的操作都自動維護這個長度標(biāo)記(方便自動擴充內(nèi)存),而WinApi如果內(nèi)部操作改變了b變量的內(nèi)容,比如"OK"改為了"OK2",由于脫離了AHK的操作,AHK內(nèi)部還是視為它的長度為2(實際為3)所以使用b變量時比如 MsgBox, %b%或a:=b就會出錯。ptr不會更新而str會更新b的長度。由于字符串有Ansi編碼,也有Unicode編碼,前者字母、數(shù)字和英文標(biāo)點符號都是占1個字節(jié),漢字占2個字節(jié),而后者所有字符都占兩個字節(jié),WinApi要處理這兩種可能的情形怎么辦呢?微軟的方法是絕大多數(shù)WinApi都提供兩個版本(分別以A或W結(jié)尾),方便使用者調(diào)用合適的一版。AHK的原生字符串(比如a:="OK")是根據(jù)AHK是Ansi版還是Unicode版分別是Ansi編碼或Unicode編碼,那么用ptr,&a的方式傳入字符串a(chǎn)的內(nèi)存地址時,怎么確保WinApi剛好需要的是AHK原生編碼呢?沒問題,AHK會智能根據(jù)自己是Ansi版還是Unicode版自動在調(diào)用函數(shù)后面加A或W,這樣就剛好了。但是如果WinApi的某個函數(shù)只有Ansi版,而AHK原生編碼為Unicode版,顯然ptr的輸入形式和str的輸入形式都會出錯,因為它們都使用原生編碼,這時可以用AHK提供的Astr類型和Wstr類型,這兩種類型明確指示了要提供給函數(shù)的字符串使用的編碼,如果指定的Astr與原生編碼不一致,則會利用一個臨時變量b,將字符串用StrPut轉(zhuǎn)換編碼到b變量中,然后把這個臨時變量的地址(&b)壓入棧中。當(dāng)然如果指定的Astr與原生編碼一致則不用轉(zhuǎn)換,直接把變量地址壓入棧。注意Astr和Wstr可能傳入的是臨時變量的地址,如果需要返回字符串,WinApi修改的也可能是臨時地址中的內(nèi)容,不能體現(xiàn)在參數(shù)的變量中來,所以這時要用ptr或者str類型,必要時手動StrPut轉(zhuǎn)換輸入需要的編碼。如果WinApi修改返回的編碼為Ansi編碼,不是當(dāng)前AHK的原生編碼時,這時自己手動轉(zhuǎn)碼 StrGet(&a,"CP0") 即可。

利用StrPut可以將原生編碼(由AHK是Ansi版還是Unicode版決定的)的字符串轉(zhuǎn)換為目標(biāo)編碼,StrGet讀取目標(biāo)編碼轉(zhuǎn)換為當(dāng)前的原生編碼。

3、WinApi返回數(shù)值一般有兩種形式,一種是函數(shù)返回值,通過寄存器返回,DllCall讀取寄存器的數(shù)值到函數(shù)返回變量,這種只能返回1個值(這時由返回類型指定讀取的字節(jié)數(shù),返回類型不對可能結(jié)果不同)。另一種是WinApi把某個數(shù)值保存到某個內(nèi)存地址中并占幾個字節(jié)(比如占1個字節(jié)對應(yīng)char類型,4字節(jié)對應(yīng)int類型,8字節(jié)對應(yīng)int64類型),AHK通過char(charp同義)傳遞一個臨時內(nèi)存地址給函數(shù),函數(shù)把數(shù)值寫入這個臨時內(nèi)存地址,函數(shù)返回后,AHK從這個臨時內(nèi)存地址讀取1個字節(jié)的數(shù)值 NumGet(臨時地址值,"char") ,這樣就實現(xiàn)了通過傳址參數(shù)來返回多個數(shù)值結(jié)果。雖然類型一般用于返回值,但如果這個char,a后面的a值也要作為輸入值對WinApi有用,AHK會在臨時內(nèi)存地址中用 NumPut(a,臨時地址值,"char") 把a的值存入這個地址,注意char限定了僅寫入1個字節(jié)的數(shù)值,范圍為-128~127,超出1個字節(jié)的部分會舍去。

4、ptr類型只是簡單傳遞了變量的內(nèi)存地址給函數(shù),它沒有str、*類型那么多內(nèi)部智能轉(zhuǎn)換操作,它主要用于傳遞一個數(shù)據(jù)結(jié)構(gòu)給函數(shù)。函數(shù)的參數(shù)往往需要特定的數(shù)據(jù)結(jié)構(gòu),因為只要得到這個結(jié)構(gòu)的首地址,按照這個結(jié)構(gòu)的約定格式,就能用首地址加偏移獲取各部分的數(shù)據(jù)了。們一般先用 VarSetCapacity(a,100) 申請一塊內(nèi)存,然后利用NumPut 按WinApi約定的格式手動把數(shù)值寫入a變量內(nèi)存中相應(yīng)的地址,數(shù)據(jù)結(jié)構(gòu)設(shè)定好后,再把&a地址傳入函數(shù),調(diào)用結(jié)束后,還可以手動用NumGet 從a變量的數(shù)據(jù)結(jié)構(gòu)中讀取需要的值。

Api的讀寫都是對內(nèi)存地址的操作,所以帶大量返回的參數(shù)一般要先用VarSetCapacity申請一塊足夠的內(nèi)存,避免亂寫內(nèi)存覆蓋了有用的數(shù)據(jù)。

四、為什么AHK的參數(shù)類型不只用Ptr和Int兩種?

我前面說過,WinAPI函數(shù)的參數(shù)數(shù)據(jù)類型,表面上五花八門,實際上基本上就是地址型Ptr(視操作系統(tǒng)自動為4或8字節(jié))和整型Int(4字節(jié))兩種,因為這是編程中表示數(shù)值的最常用類型,如果有哪個奇葩的程序員為了節(jié)約一點點??臻g,在傳入簡單數(shù)值時,用到了16位整型Short(2字節(jié))和8位整型Char(1字節(jié)),那我真是服了他了,這是非常罕見的。

因此,我們不是可以用Ptr和Int走遍天下了?先看看WinAPI的參數(shù)類型聲明,然后簡單判斷一下是地址還是簡單數(shù)值,簡單數(shù)值有個特殊的SIZE_T類型是為了輸入可變類型的數(shù)值的,在Win32位系統(tǒng)為4字節(jié),在Win64系統(tǒng)為8字節(jié),所以我們記住把它設(shè)為自適應(yīng)的Ptr類型,其他的簡單數(shù)值,除了Int64、Long Long、Double這些明確的8字節(jié)類型我們用AHK的Int64類型以外,其他的都用4字節(jié)的Int類型就基本上不會錯了。(浮點數(shù)即小數(shù),比較特殊,應(yīng)當(dāng)使用Double類型表示8字節(jié),用Float類型表示4字節(jié),這種在函數(shù)的聲明中很容易判斷)于是前面調(diào)用Dll文件中的InStr函數(shù)的例子寫成下面的參數(shù)類型也不錯:

Pos:=DllCall("Dll文件\InStr", "Ptr",&(s1:="abc123"), "Ptr",&(s2:="abc"), "Int")

為什么AHK還要更多地設(shè)立Str類型和類型呢?因為Ptr和Int兩種類型只是死板地傳遞數(shù)值,沒有多余動作,而Str類型和類型都有神奇的調(diào)用前后內(nèi)部轉(zhuǎn)換操作,且聽我一一道來。

五、Str字符串類型的好處。

1、首先我們要認識到字符串在內(nèi)存中是以什么形式存在的。

AHK把除了對象以外的變量都保存為字符串,比如a:="123",a:=123,在內(nèi)存中都保存為字符串形式。

怎么查看字符串的內(nèi)存值呢?

我們知道“&a”是獲取a變量的內(nèi)存首地址,*是讀取內(nèi)存地址的1字節(jié)值的操作符,我們運行下面的代碼看看效果:


a:=12, p:=&a, n1:=p, n2:=(p+1), n3:=(p+2), n4:=(p+3), n5:=(p+4), n6:=(p+5)

MsgBox, %n1% %n2% %n3% %n4% %n5% %n6% ;-- 顯示結(jié)果為:49 0 50 0 0 0


這些數(shù)字代表什么含義呢?1的ASCII值為49,2的ASCII值為50,由于我的AHK是Unicode版本的,Unicode版本的原生字符串(AHK中可用的)都是用兩字節(jié)表示任何字符編碼,

所以49 0占兩個字節(jié),50 0也占兩個字節(jié),最后兩個字節(jié)0 0表示字符串的結(jié)尾\0字符。

如果AHK是ANSI版本的,原生字符串就是ANSI編碼,英文和英文標(biāo)點符號都占一個字節(jié),而漢字等語言的編碼一個字占兩個字節(jié),字符串的結(jié)尾用一個字節(jié)0表示結(jié)束\0字符。

2、WinAPI函數(shù)怎么讀取字符串參數(shù)。

前面說了,字符串參數(shù)壓入棧中的是字符串的內(nèi)存首地址,也就是"Ptr",&a這種形式。但是假如WinAPI函數(shù)的參數(shù)需要ANSI編碼的字符串,而AHK版本為Unicode編碼怎么辦?

使用原生編碼顯然錯誤,這時有兩種方法,一種是手動轉(zhuǎn)換編碼,利用StrPut()把Unicode的編碼轉(zhuǎn)換成ANSI編碼保存到b變量的內(nèi)存地址中,然后"Ptr",&b傳遞參數(shù)。

另一種方法就是利用AHK提供的AStr參數(shù)類型,它會在調(diào)用前自動把參數(shù)的原生字符串在臨時變量的內(nèi)存中轉(zhuǎn)為ANSI編碼并把臨時變量的內(nèi)存首地址壓入棧中。還有一個WStr參數(shù)類型,可以在調(diào)用前自動把ANSI編碼的原生字符串轉(zhuǎn)換為Unicode編碼,再把臨時變量的內(nèi)存首地址壓入棧中。當(dāng)然,如果原生變量與AStr/WStr指定的一致,就不用轉(zhuǎn)換,直接把a變量的內(nèi)存首地址壓入棧中,等效于"Ptr",&a 。

AHK采用了更聰明的方法確保原生編碼符合WinAPI的需求,因為WinAPI為了適應(yīng)兩種字符串編碼,大多數(shù)函數(shù)都有A/W結(jié)尾的兩個版本(如DeleteFileA、DeleteFileW),AHK讀取函數(shù)名稱時如果找不到DeleteFile,會自動根據(jù)自身是ANSI編碼還是Unicode編碼在函數(shù)名稱后面加A或W,如果WinAPI準(zhǔn)備了這兩種版本的,就剛好智能匹配了。由于AHK有這種智能匹配機制,所以一般用原生的Str類型(不轉(zhuǎn)換)就行了。用它的好處,一是可以直接采用字符串(比如"Str","abc123"),對于變量也不用取地址&。另一個更重要的好處是,調(diào)用結(jié)束后,會更新對應(yīng)變量的字符串長度。

3、Str類型可以更新變量的字符串長度。

由于AHK是自動管理內(nèi)存的,變量占用的內(nèi)存經(jīng)常變動,需要增大內(nèi)存時就要動態(tài)申請內(nèi)存然后把舊的內(nèi)容拷貝過去,把變量的地址設(shè)到新的內(nèi)存地址上,而字符串的內(nèi)存大小體現(xiàn)在字符串的長度上,所以AHK內(nèi)部標(biāo)記了每個字符串變量的長度。AHK自身對字符串的改變操作,比如賦值、替換等都會自動調(diào)整這個長度標(biāo)記。而調(diào)用WinAPI中的函數(shù),由于控制權(quán)不在AHK手中,發(fā)生了什么它也不知道,如果原來的字符串為a:="abc123",但是如果WinAPI內(nèi)部操作在末尾添加了"456"(或者把a的內(nèi)存內(nèi)容改為了"xyz\0"),實際上a:="abc123456"(或者a:="xyz"),而用b:=a,或者MsgBox, %a%來讀取a的值時,AHK內(nèi)部沒有更新a的長度,還認為字符串長度為6,就會造成錯誤。Str形式會更新字符串長度,而Ptr形式不會更新。

注1:Ptr形式可以用VarSetCapacity(a,-1)或者StrGet(&a)兩種方式手動更新長度。

注2:Astr和Wstr可能傳入的是臨時變量的地址,如果需要返回字符串,WinApi修改的也可能是臨時地址中的內(nèi)容,不能體現(xiàn)在參數(shù)的變量所在的內(nèi)存地址中來,所以如果需要返回字符串,還是要用Ptr或者Str類型,因為這兩種類型,壓入棧中的地址就是變量的內(nèi)存首地址(沒有經(jīng)過任何轉(zhuǎn)換,必要時需要手動轉(zhuǎn)換成正確的編碼)。

如果WinApi返回的字符串編碼與AHK原生編碼不同時,需要自己手動用StrGet()轉(zhuǎn)碼。

六、*類型用于從參數(shù)獲取函數(shù)返回數(shù)值。

1、WinApi通過函數(shù)的返回值可以返回單個數(shù)值。通過寄存器(EAX)返回,DllCall讀取寄存器的數(shù)值到函數(shù)返回變量,這時由返回類型指定讀取的字節(jié)數(shù),

返回類型一般是地址型Ptr或者整型Int兩種,比較特殊的是Str返回類型,AHK會把返回的數(shù)值看做字符串的內(nèi)存首地址,并復(fù)制字符串到返回變量中。

2、WinAPI通過參數(shù)變量本身的內(nèi)存地址可以返回多個數(shù)值,類似于ByRef類型。WinApi把某個數(shù)值保存到某個內(nèi)存地址中并占幾個字節(jié)(比如占1個字節(jié)對應(yīng)

Char類型,4字節(jié)對應(yīng)Int類型,8字節(jié)對應(yīng)Int64類型),AHK不直接把參數(shù)變量的內(nèi)存地址通過"Ptr", &a傳給WinAPI,而是通過Char(CharP同義)傳遞一個臨時內(nèi)存地址給WinAPI函數(shù),函數(shù)把數(shù)值寫入這個臨時內(nèi)存地址,函數(shù)返回后,AHK自動從這個臨時內(nèi)存地址讀取1個字節(jié)的數(shù)值到變量a,這樣就實現(xiàn)了通過傳遞臨時地址的參數(shù)來返回數(shù)值結(jié)果。雖然類型一般用于返回值,但如果這個Char,a后面的a值也要作為輸入值對WinApi有用,AHK會在臨時內(nèi)存地址中用把a的值存入這個地址,注意char限定了僅寫入1個字節(jié)的數(shù)值。

3、用Ptr代替*類型不可取。

如果用 "Ptr",&a 傳遞變量的內(nèi)存地址給函數(shù)來接收返回數(shù)值可不可行呢?首先考慮傳遞的地址中如果先需要一個輸入值,這時要自己手動采用NumPut()寫入到地址&a中。假如我們設(shè)置a:=1,它不是已經(jīng)是數(shù)值了嗎,怎么還要NumPut()呢?因為AHK內(nèi)部把數(shù)值變量也都保存為字符串,所以a的內(nèi)存首地址中保存的是1的字符串編碼,即Asc("1")==>49,所以必須自己手動NumPut(1,a,"char")。函數(shù)返回后,雖然WinApi函數(shù)確實把返回數(shù)值寫入到&a地址中了,但是我們要讀取出來的其實是字符串表示的數(shù)值,這才能用于AHK中,于是又要手動NumGet()讀取。

七、利用Ptr類型輸入數(shù)據(jù)結(jié)構(gòu)。

Ptr類型只是簡單傳遞了變量的內(nèi)存地址給函數(shù),它沒有str、*類型那么多的內(nèi)部智能轉(zhuǎn)換操作,它主要用于傳遞一個數(shù)據(jù)結(jié)構(gòu)的地址給函數(shù)。

函數(shù)的參數(shù)往往需要特定的數(shù)據(jù)結(jié)構(gòu),因為只要得到這個結(jié)構(gòu)的首地址,按照這個結(jié)構(gòu)的約定格式,就能用內(nèi)存首地址加偏移獲取各部分的數(shù)據(jù)了。我們一般先用 VarSetCapacity(a,100) 申請一塊內(nèi)存,然后使用NumPut() 按WinApi約定的數(shù)據(jù)結(jié)構(gòu)手動把數(shù)值寫入a變量內(nèi)存對應(yīng)的地址中,數(shù)據(jù)結(jié)構(gòu)設(shè)定好后,再把&a地址傳入函數(shù)。調(diào)用結(jié)束后,還可以手動使用 NumGet() 從a變量的數(shù)據(jù)結(jié)構(gòu)中讀取需要的值。NumPut()、NumGet()都是AHK對內(nèi)存的指針操作,&取變量內(nèi)存地址也是指針。WinApi的讀寫都是對內(nèi)存地址的操作,所以在調(diào)用前一般要先用VarSetCapacity()申請足夠的內(nèi)存,避免WinApi亂寫內(nèi)存覆蓋了有用的數(shù)據(jù)。

八、其他說明:

1、調(diào)用約定:C語言寫的函數(shù),返回類型前一般要添加"Cdecl",而WinApi使用標(biāo)準(zhǔn)調(diào)用形式則不用添加。若C函數(shù)編譯時指定了使用標(biāo)準(zhǔn)調(diào)用也不用。"C"調(diào)用約定是棧的平衡由調(diào)用者來完成,調(diào)用者壓入了多個參數(shù)到棧中,最后棧頂指針的恢復(fù)要由調(diào)用者來做。而標(biāo)準(zhǔn)調(diào)用則要函數(shù)自己來恢復(fù),掉用者只管壓棧不管恢復(fù)。所以如果調(diào)用C函數(shù)不加上"Cdecl",默認使用標(biāo)準(zhǔn)調(diào)用,棧的平衡無法完成,多次調(diào)用后會耗盡棧資源。

2、函數(shù)返回類型不是很重要,對Api運行沒有影響,只對AHK讀取函數(shù)返回值有影響(使用 NumGet 讀?。?。比較特殊的是str返回類型,AHK會把返回的數(shù)值看做字符串首地址,并復(fù)制字符串到返回變量中(使用 StrGet 讀?。?/p>

3、U前綴指示了使用無符號的類型,這對于輸入數(shù)值沒有意義(int64除外),因為輸入時指定char和uchar,NumPut寫入內(nèi)存的都是同樣的數(shù)值,但對于Api 的輸出數(shù)值,即 函數(shù)返回類型 和 char*類型 就有意義了,AHK內(nèi)部調(diào)用NumGet讀取的值可能不同。

4、Windows數(shù)據(jù)類型對應(yīng)于AHK參數(shù)類型的簡單判斷:(懂了上面的就不難了)簡單數(shù)值的WinApi參數(shù)絕大部分對應(yīng)int類型,比如:DWORD、LONG、BOOL、COLORREF。浮點類型對應(yīng)float、double。帶64的、LONGLONG對應(yīng)int64。比較特殊的是SIZE_T類型,它在32位系統(tǒng)中對應(yīng)int,在64位系統(tǒng)中對應(yīng)int64,所以要用AHK自適應(yīng)的ptr類型來對應(yīng)。內(nèi)存地址的WinApi參數(shù)對應(yīng)AHK的三種形式,一般WinApi聲明中的各種句柄(H開頭的)、帶LP或P開頭的、帶PTR的都是指針,即地址類型,一般對應(yīng)ptr類型。但是帶STR的指針則對應(yīng)str類型更方便些。如果是用于輸出結(jié)果的指針就對應(yīng)*類型。對于輸入類型U前綴不重要,因此uint寫成int也沒問題。

內(nèi)存地址的WinApi參數(shù)對應(yīng)于AHK的三種形式:一般WinApi聲明中的各種句柄(H開頭的)、帶LP或P開頭的、帶PTR的都是指針,即地址類型,一般對應(yīng)Ptr類型。但是帶STR的指針則對應(yīng)Str類型更方便些(用Ptr就稍麻煩,參看上面的說明)。

如果是用于輸出結(jié)果的指針就對應(yīng)*類型(用Ptr就不可取,參看上面的說明)。

九、舉個簡單的例子

請對照前面講的來理解。GetUserName是一個WinApi函數(shù),用于獲得當(dāng)前windows登錄的用戶名。
我們先看看MSDN網(wǎng)站的權(quán)威聲明:
https://msdn.microsoft.com/en-us/library...s.85).aspx


BOOL WINAPI GetUserName(
Out LPTSTR lpBuffer,
Inout LPDWORD lpnSize
);
DLL | Advapi32.dll
Unicode and ANSI names | GetUserNameW (Unicode) and GetUserNameA (ANSI)


從上面的聲明我們知道,這個函數(shù)位于Advapi32.dll庫文件中,它在庫文件中有兩個版本名稱,分別是GetUserNameW處理Unicode字符串,GetUserNameA處理ANSI字符串。

我前面說過,WinApi凡是牽涉到字符串的函數(shù),大多都提供了A和W結(jié)尾的兩個版本的函數(shù)供用戶使用,AHK可以根據(jù)自身是ANSI版還是Unicode版,在找不到函數(shù)時會智能在函數(shù)名末尾添加A或W,從而智能找到剛好匹配的WinApi函數(shù)。所以,AHK調(diào)用的第一部分為:DllCall("Advapi32.dll\GetUserName" 就好了。

當(dāng)然因為我的AHK為Unicode版,我直接使用GetUserNameW也行,AHK能夠直接找到這個函數(shù)名,就不會在末尾添加A或W再嘗試了。

如果直接使用GetUserNameA行不行呢?由于AHK能夠直接找到這個函數(shù)名,同樣不會在末尾添加A或W再嘗試了。這樣該函數(shù)在處理輸入輸出時,內(nèi)部默認都是ANSI編碼的字符串,所以對于輸入字符串,我們需要將AHK原生的Unicode編碼字符串用StrPut轉(zhuǎn)換成ANSI編碼字符串,再把字符串首地址傳給該函數(shù),對于輸出字符串,需要用StrGet將返回的字符串首地址轉(zhuǎn)換為Unicode字符串。

廢話不多說,我們再看看它的參數(shù)。它有兩個參數(shù),我們一一分析。

第一個參數(shù) “Out LPTSTR lpBuffer”,用于返回Windows登錄的用戶名字符串。這個參數(shù)是輸出的,以LP開頭的都是指針,也就是字符串首地址,毫無疑問,我們用AHK的地址類型“Ptr”作為參數(shù)類型是可以的。我前面說過,對于含有STR
的指針,使用“Str”類型會更方便些,下面會詳細分析。

第二個參數(shù) “Inout LPDWORD lpnSize”,用于設(shè)置處理的字符串長度。這個參數(shù)既是輸入一個長度最大值,又是輸出結(jié)果的長度值,以LP開頭的都是指針,毫無疑問,我們用AHK的地址類型“Ptr”作為參數(shù)類型是可以的。但是我前面說過,對于輸出數(shù)值的地址參數(shù),使用“”類型可以自動轉(zhuǎn)換,即輸入時,將AHK常規(guī)的字符串型數(shù)字,用NumPut寫入一個臨時內(nèi)存地址,返回時,從臨時內(nèi)存地址用NumGet讀取數(shù)值轉(zhuǎn)換為字符串型數(shù)字,供AHK常規(guī)使用。所以這個參數(shù)用“Ptr”類型不妥,使用“Int”則剛剛好。使用無符號的“UInt*”也可以,從臨時內(nèi)存讀取數(shù)值時,有符號的Int類型,因為32位二進制中第一位表示正負符號,只有31位表示數(shù)值,所以最大范圍為2147483647 (0x7FFFFFFF),而無符號的UInt類型表示的數(shù)值最大范圍為4294967295 (0xFFFFFFFF),范圍翻了一倍。但是對于我們這個函數(shù)要返回的登錄用戶名字符串長度而言,長度只有幾十字節(jié),所以沒必要用UInt。不過Windows的DWORD類型其實對應(yīng)于無符號的“UInt”。

參數(shù)類型搞清楚了,就可以寫出調(diào)用格式了:
DllCall("Advapi32.dll\GetUserName", "Str",name, "Int*",size)
最后用消息框顯示結(jié)果:

MsgBox, % "登錄用戶名:" name " `n字符串長度:" size

這樣就可以了嗎?

不行。我前面說過,WinApi都是對內(nèi)存地址的操作,所以在調(diào)用前一般要先用VarSetCapacity()申請足夠的內(nèi)存,避免WinAPI亂寫內(nèi)存覆蓋了有用的數(shù)據(jù)。這個函數(shù)把返回用戶名字符串寫入name變量的內(nèi)存地址,但是我們的AHK的變量初始都為空的,內(nèi)部合法占用的內(nèi)存為0字節(jié),明顯不夠WinApi寫入的。為了可靠地讓W(xué)inApi操作合法的內(nèi)存,我們用VarSetCapacity(name,100)手動申請100字節(jié)的內(nèi)存給name變量合法占用,因為100字節(jié)內(nèi)存對于ANSI編碼的英文可以寫入99個字母(加上末尾的“\0”字符),對于Unicode字符可以寫入49個字符,應(yīng)該夠用了。所以最后完整的調(diào)用代碼為:

size:=100, VarSetCapacity(name, size) 
DllCall("Advapi32.dll\GetUserName", "Str",name, "Int*",size) 
MsgBox, % "登錄用戶名:`t" name "`n`n字符串長度:`t" size

這個函數(shù)其實有個返回類型“BOOL”,用于返回函數(shù)調(diào)用是否成功,對應(yīng)于AHK的Int類型,由于我們不需要該返回值,所以可以忽略返回類型參數(shù)那部分。

最后我們再假設(shè)WinApi只有GetUserNameA這一個函數(shù),而我們的AHK又是Unicode版的情況。對于輸入字符串參數(shù),比如 a:="abc",我們只要使用"AStr",a 即可讓AHK幫我們自動轉(zhuǎn)換為臨時變量的ANSI字符串傳給WinApi。但是這個函數(shù)需要從參數(shù)的地址返回字符串,所以不能使用“AStr”類型,需要手動轉(zhuǎn)換,可以這樣做:

a:="abc" 
VarSetCapacity(b, StrLen(a)*2+100) ;-- 6字節(jié)不夠用 StrPut(a, &b, "CP0")

這樣b變量的內(nèi)存地址就有了ANSI編碼的字符串內(nèi)容,然后:

DllCall("Advapi32.dll\GetUserNameA", "Ptr",&b, "Int*",100)

把b變量的內(nèi)存首地址傳給WinApi,最后讀取WinApi的返回結(jié)果時還要把ANSI編碼的結(jié)果轉(zhuǎn)換回Unicode編碼供AHK使用:

name:=StrGet(&b,"CP0") 
MsgBox, % "登錄用戶名:`t" name

這樣就大功告成了。雖然麻煩點,但是對于WinApi庫函數(shù)中沒有與AHK本身Unicode還是ANSI版匹配的情況,只好這樣手動解決。

由于例子比較簡單,沒有牽涉到WinApi參數(shù)常見的傳入數(shù)據(jù)結(jié)構(gòu)。其實構(gòu)造數(shù)據(jù)結(jié)構(gòu)很簡單,先用 VarSetCapacity(a,16) 申請內(nèi)存,再用 NumPut(寫入數(shù)值, a, 偏移字節(jié), "Int") 根據(jù)WinApi的數(shù)據(jù)結(jié)構(gòu)要求寫入數(shù)值到正確的偏移位置,然后傳入 "Ptr",&a 給WinApi即可。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容