Autohotkey 中文论坛
DllCall的使用简介 - 可打印的版本

+- Autohotkey 中文论坛 (http://ahkcn.net)
+-- 版块: AutoHotkey (/forum-3.html)
+--- 版块: 教程和文档 (/forum-14.html)
+--- 主题: DllCall的使用简介 (/thread-6078.html)



DllCall的使用简介 - feiyue - 2016-06-07 01 : 31


;--------------------------------
; DllCall的使用简介 By FeiYue
;--------------------------------

DllCall是AHK的一个强大功能,用来调用Dll文件中的函数,例如标准的WinAPI函数。
许多新手可能觉得DllCall很复杂、很难用,于是对强大的WinAPI函数不敢上手,
只能羡慕那些高手们调用WinAPI实现各种功能。其实DllCall的使用并不难,下面我就
为大家拨开DllCall的神秘面纱,其实你也能简单学会!(下面都以WinAPI函数为例)

DllCall调用的格式为:
Result := DllCall("[DllFile\]Function" [, Type1,Arg1, Type2,Arg2, "Cdecl ReturnType"])

我们都用过AHK的内置函数或自己写的函数,比如:Pos:=InStr("abc123", "abc")
假如一个Dll文件中也有一个InStr函数,而且那个函数也需要两个字符串参数,怎么调用呢?
调用的格式为:Pos:=DllCall("Dll文件\InStr", "Str","abc123", "Str","abc", "Int")

我们比较一下区别:
AHK函数的函数名InStr,相当于DllCall调用中的 DllCall("Dll文件\InStr" 部分;
AHK函数的两个参数,在DllCall调用中每个参数前面都伴随了一个说明参数类型的参数;
最后DllCall调用的尾部还多出了一个说明函数返回类型的参数。
函数名部分很容易照搬使用,函数返回类型也只有两三种比较容易,只有参数类型有点难度。

要学会并自由使用DllCall需要进修三步:

第一步,要学会准确设定参数类型,这是重点、难点。

第二步,要学会用VarSetCapacity()分配内存。当我们使用一个新变量a并赋值时,
AHK自动为a变量申请了一块内存,但大小是尽量小的,不由我们掌控。
因此我们可以利用VarSetCapacity(a,字节数)来自己申请指定大小的内存。

要了解&为取变量内存地址的操作符,及用NumGet()、NumPut()读写内存数据,
获取内存地址及读写内存数据都相当于C语言的指针操作。

第三步,了解字符串在内存中的编码状态,要学会用StrGet()、StrPut()转换字符串编码。
利用StrPut可以将原生编码(由AHK是ANSI版还是Unicode版决定的)
的字符串转换为目标编码,StrGet读取目标编码转换为当前的原生编码。

要明白AHK将数字也保存为字符串格式。我们把数字赋值给变量,例如a:=1,
假如把1的数值直接保存在a变量对应的内存地址中,那么就要区分变量
是占1个字节(Char),还是2个字节(Short),还是4个字节(Int),
还是8个字节(Int64),这样的话,每个变量使用前都必须先定义它的数据类型
(指明占用字节数)了,比如C语言就要用“int a;”定义一个4字节的变量。
而AHK作为脚本程序,为了方便用户免声明任意使用变量,所以AHK把数字都
在内存中保存为字符串格式,因为字符串的末尾都有“\0”作为结束标记。
(Ansi编码“\0”为1个字节值0,Unicode编码“\0”为2个字节值0)

第二、三步看帮助文件就很容易懂了,第一步我慢慢讲解,引导大家深入理解参数类型。

一、为什么每个参数前面都要设定参数类型?

1、DllCall调用的大概流程是:DllCall首先把Dll文件整个读取到AHK进程的私有内存中,
然后通过函数名字符串找到对应的函数入口地址,然后把参数一个一个压入AHK进程的栈中
(每个参数占A_PtrSize个字节,32位系统中要传入64位的值时占A_PtrSize*2个字节),
然后跳转到函数的入口地址,控制权交给了入口地址的机器码手中。机器码会从栈中
把参数的数值一个一个读取回来,然后执行自身的代码。机器码执行完毕后,会把返回值
写入通用寄存器EAX中,然后把控制权归还给AHK,然后AHK从通用寄存器EAX中读取返回值。

2、我们知道AHK脚本是动态解析的,AHK函数的参数都用逗号作为分隔符隔开,不用考虑参数
的数据类型,而DllCall调用函数,要把参数的数值连续压入栈中,中间可是没有分隔符的,
如果每个参数前面不设定参数类型,各个参数怎么准确分隔开就有点困难。

如果默认采用每个参数都是A_PtrSize个字节的整型数据,那么有两种情况不能解决:
一是在Win32位系统中如果需要输入一个8字节的整型参数,必须标记为Int64类型;
二是如果要输入4字节的Float或8字节的Double类型的浮点数,必须标记为浮点类型,
因为浮点数在内存中的保存形式与一般的整型数不同,浮点数不能以整型数的形式压入栈中。

3、编写WinAPI函数的程序员不会为了省事便来个默认约定,该用地址型就地址型
(这在AHK中对应Ptr类型,会根据AHK自身是32位版还是64位版自动为4字节或8字节),
该用整型就整型(这在AHK中对应Int占用4字节),该用浮点型就浮点型(对应Float),
然后函数发布时会声明各个参数的参数类型(表面上五花八门,实际上是为了说明
每个参数在栈中占用A_PtrSize*N个字节,是否为浮点数,或者该参数是否为返回值)。

DllCall调用时就要按照WinAPI函数的声明,在每个参数前面加上合适的参数类型,
如果参数类型错误,机器码从栈中读取参数时就会错位读到错误的值,容易造成崩溃。

小结:
AHK的数据类型看起来很复杂,但只在操作数据结构(NumPut及NumGet),
和获取返回值(一是整个函数的返回值,二是星号类型的参数)时才有用,
对于传入的参数没什么用。因为每个参数传入系统的栈时至少占用A_PtrSize个字节,
所以AHK数据类型不够A_PtrSize个字节时,会先把此类型扩充为UPtr类型再传递数值。
也就是说("char",-1)与("short",-1)与("int",-1)与("UPtr",-1)是一样的。

简单来看,所有传入的参数都使用"Ptr"类型是可以的(太简单了!),
只要注意两种例外情况:一是32位系统中传入64位的值Int64,二是传入浮点数Float。

所以,先看看WinAPI的参数类型声明,简单判断一下是否是Int64、Long Long
这些明确的8字节类型采用Int64类型,再看看是否是浮点数类型,用AHK的
4字节Float类型或8字节Double类型,其他都用Ptr即可(用int、uint也是等效的)。

二、为什么AHK的参数类型不只采用Ptr和Float、Int64三种?

根据前面讲的,调用Dll文件中的InStr函数的例子写成下面的参数类型也不错:
Pos:=DllCall("Dll文件\InStr", "Ptr",&(s1:="abc123"), "Ptr",&(s2:="abc"), "Int")
为什么AHK要把地址类型(Ptr)分解成了Ptr类型、Str类型和*类型三种呢?

因为Ptr类型只是死板地传递数值,没有多余动作。而Str类型和*类型都有神奇的
调用前后的内部自动转换操作,可以很方便地通过参数接收返回结果,所以很好用。

三、Str字符串类型的好处。

1、首先我们要认识到字符串在内存中是以什么形式保存的。
在AHK中我们用一对双引号包围的一些文字来表示字符串,比如"abc123"。
其实AHK在解析的时候还是会把它赋值给某个变量的,例如 a:="abc123"。
a变量代表一块内存,字符串保存到这块内存中变成了一个一个的数值,
称为字符编码。最初发明编程的人,由于仅使用英文26个字母和数字、
英文符号,加起来不到256个,所以规定了一套ASCII编码,每个字母、
数字等的编码都占1个字节。

后来要推广到所有国家,仅用英文字母肯定不够,所以各国都推出了
自己国家的编码,例如中国的汉字,1个汉字就用两个字节来编码。
通过各国编码时采用不同的码区,综合起来就形成了Ansi编码,
即英文的字母和数字、符号还是用1个字节的ASCII码,其他国家
的编码可能用一个或两个字节来编码。

由于编码越来越多,也比较乱,微软为了大一统,推出了一种Unicode
编码,即所有的字符都用两个字节来编码,并囊括了所有国家的文字。

目前Ansi编码和Unicode编码都很常用,因此WinAPI处理字符串时,
基本上同时提供两套API函数,分别以A和W结尾,对应两种编码。

AHK把除了对象以外的变量都保存为字符串,比如a:="123",a:=123,在内存中都保存为
字符串形式。怎么查看字符串的编码值呢?我们知道“&a”是获取a变量的内存首地址,
*是读取内存地址的1字节值的操作符,我们运行下面的代码看看效果:
-----------------------------
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% ;-- 显示结果为:49 0 50 0 0 0
-----------------------------
这些数字代表什么含义呢?1的ASCII值为49,2的ASCII值为50,由于我的AHK是Unicode
版本的,Unicode版本的原生字符串(AHK中可用的)都是用两字节表示任何字符编码,
所以49 0占两个字节,50 0也占两个字节,最后两个字节0 0表示字符串的结尾\0字符。
如果AHK是ANSI版本的,原生字符串就是ANSI编码,英文和英文标点符号都占一个字节,
而汉字等语言的编码一个字占两个字节,字符串的结尾用一个字节0表示结束\0字符。

2、WinAPI函数读取字符串时要注意编码匹配。
前面说了,字符串参数压入栈中的是字符串的内存首地址,也就是"Ptr",&a这种形式。
但是假如WinAPI函数的参数需要ANSI编码的字符串,而AHK版本为Unicode编码怎么办?
使用原生编码显然错误,这时有两种方法,一种是手动转换编码,利用StrPut()把
Unicode的编码转换成ANSI编码保存到b变量的内存地址中,然后用"Ptr",&b传递参数。
另一种方法就是利用AHK提供的AStr参数类型,它会在调用前自动把参数的原生字符
串在临时变量的内存中转为ANSI编码并把临时变量的内存首地址压入栈中。
还有一个WStr参数类型,可以在调用前自动把ANSI编码的原生字符串转换为Unicode
编码,再把临时变量的内存首地址压入栈中。当然,如果原生变量与AStr/WStr指定
的一致,就不用转换,直接把a变量的内存首地址压入栈中,等效于"Ptr",&a 。

AHK采用了更聪明的方法确保原生编码符合WinAPI的需求,因为WinAPI为了适应两种
字符串编码,大多数函数都有A/W结尾的两个版本(如DeleteFileA、DeleteFileW),
AHK读取函数名称时如果找不到DeleteFile,会自动根据自身是ANSI编码还是Unicode
编码在函数名称后面加A或W,如果WinAPI准备了这两种版本的,就刚好智能匹配了。

由于AHK有这种智能匹配机制,所以一般用原生的Str类型(或Ptr,&a)就行了。
用Str的好处,一是可以直接采用字符串(比如"Str","abc123"),对于变量也不用
取地址&。另一个更重要的好处是,调用结束后,会更新对应变量的字符串长度。

3、Str类型可以更新变量的字符串长度,这对于通过变量地址返回字符串很好用。
由于AHK是自动管理内存的,变量占用的内存经常变动,需要增大内存时就要动态
申请内存然后把旧的内容拷贝过去,把变量的地址设到新的内存地址上,而字符串
的内存大小体现在字符串的长度上,所以AHK内部标记了每个字符串变量的长度。
AHK自身对字符串的改变操作,比如赋值、替换等都会自动调整这个长度标记。
而调用WinAPI中的函数,由于控制权不在AHK手中,发生了什么它也不知道,如果
原来的字符串为a:="abc123",但是如果WinAPI内部操作在末尾添加了"456\0"
(或者把a的内存内容改为了"xyz\0"),实际上a:="abc123456"(或者a:="xyz"),
而用b:=a,或者MsgBox, %a%来读取a的值时,AHK内部没有更新a的长度,还认为
字符串长度为6,就会造成错误。Str形式会更新字符串长度,而Ptr形式不会更新。

注1:如果不需要自动转换编码,那么Ptr,&a等效于Str,a。如果需要返回字符串,
Ptr,&a形式可以用VarSetCapacity(a,-1)或者StrGet(&a)两种方式手动更新长度。

注2:Astr和Wstr可能传入的是转换编码后的临时变量的地址,如果需要返回字符串,
WinAPI修改的也可能是临时地址中的内容,不能体现在参数的变量所在的内存地址中来,
所以如果需要返回字符串,就不能使用自动转换编码的Astr和Wstr,而要用Str或者
Ptr类型,因为这两种类型,压入栈中的地址就是变量的内存首地址。
如果WinAPI参数需要的编码不同于AHK的原生编码,需要手动用StrPut()转换成目标编码。
如果WinAPI返回的字符串编码与AHK原生编码不同,需要手动用StrGet()转换成原生编码。

四、*类型用于从参数获取函数返回数值。

1、WinAPI通过参数传入的内存地址可以返回多个数值,类似于ByRef类型。
WinAPI把某个数值保存到某个内存地址中并占几个字节(比如占1个字节对应
Char类型,4字节对应Int类型,8字节对应Int64类型),AHK不直接把参数
变量的内存地址通过"Ptr",&a传给WinAPI,而是通过Char*(CharP同义)
传递一个临时内存地址给WinAPI函数,函数把数值写入这个临时内存地址,
函数返回后,AHK自动从这个临时内存地址读取1个字节的数值到变量a,
这样就实现了通过传递临时地址的*参数来返回数值结果。虽然*类型一般用于
返回值,但如果这个Char*,a后面的a值也要作为输入值对WinAPI有用,AHK会在
临时内存地址中把a的值存入这个地址,注意char限定了仅写入1个字节的数值。

2、用Ptr代替*类型不可取。
如果用 "Ptr",&a 传递变量的内存地址给函数来接收返回数值可不可行呢?
首先考虑传递的地址中如果先需要一个输入值,这时要自己手动采用NumPut()写入
到地址&a中。假如我们设置a:=1,它不是已经是数值了吗,怎么还要NumPut()呢?
因为AHK内部把数值变量也都保存为字符串,所以a的内存首地址中保存的是1的
字符串编码,即 Asc("1")==>49,所以必须自己手动NumPut(1,a,"char")。
函数返回后,虽然WinAPI函数确实把返回数值写入到&a地址中了,但是我们要读取
出来的其实是字符串表示的数值,这才能用于AHK中,于是又要手动NumGet()读取。

五、利用Ptr类型输入数据结构。

Ptr类型只是简单传递了变量的内存地址给函数,它没有str、*类型那么
多的内部智能转换操作,它主要用于传递一个数据结构的地址给函数。
函数的参数往往需要特定的数据结构,因为只要得到这个结构的首地址,
按照这个结构的约定格式,就能用内存首地址加偏移获取各部分的数据了。

我们一般先用 VarSetCapacity(a,100) 申请一块内存,然后使用
NumPut() 按WinAPI约定的数据结构手动把数值写入a变量内存对应的
地址中,数据结构设定好后,再把&a地址传入函数。调用结束后,
还可以手动使用 NumGet() 从a变量的数据结构中读取需要的值。
NumPut()、NumGet()都是AHK对内存的指针操作,&取变量内存地址也是指针。

WinAPI的读写都是对内存地址的操作,所以在调用前一般要先用VarSetCapacity()
申请足够的内存,避免WinAPI乱写内存覆盖了有用的数据。

六、其他说明:

1、返回类型:WinAPI通过函数的返回值可以返回1个数值。通过寄存器EAX返回,
DllCall读取寄存器的数值到函数返回变量,这时由返回类型指定读取的字节数,
返回类型一般是地址型Ptr或者整型Int两种,比较特殊的是Str返回类型,
AHK会把返回的数值看做字符串的内存首地址,并复制字符串到返回变量中。

2、调用约定:C语言写的函数,返回类型前一般要添加"Cdecl",而WinAPI
使用标准调用形式则不用添加。若C函数编译时指定了使用标准调用也不用。

"C"调用约定是栈的平衡由调用者来完成,调用者压入了多个参数到栈中,
最后栈顶指针的恢复要由调用者来做。而标准调用则要函数自己来恢复,掉用者
只管压栈不管恢复。所以如果调用C函数不加上"Cdecl",默认使用标准调用,
栈的平衡无法完成,多次调用后会耗尽栈资源。

3、U前缀:指示使用无符号的类型,这对于输入数值没有意义(int64除外),
因为输入时指定char和uchar,写入内存的都是同样的数值,但对于WinAPI
的输出数值,即 函数返回类型 和 *类型 就有意义了,AHK内部读取的值
可能不同(类似于用NumGet()读取)。

4、Windows数据类型对应于AHK参数类型的简单判断。(懂了上面的就不难了)

简单数值的WinAPI参数绝大部分对应Int类型,比如:DWORD、LONG、BOOL、
COLORREF。带64的、LONGLONG对应Int64。浮点类型对应Float、Double。
对于输入类型U前缀不重要,因此UInt写成Int也没关系。

内存地址的WinAPI参数对应于AHK的三种形式:一般WinAPI声明中的各种句柄
(H开头的)、带LP或P开头的、带PTR的都是指针,即地址类型,一般对应Ptr类型。
但是带STR的指针则对应Str类型更方便些(用Ptr可行但稍麻烦,参看上面的说明)。
如果是用于输出结果的指针就对应*类型(用Ptr就不可取,参看上面的说明)。




RE: DllCall的使用简介 - feiyue - 2017-07-11 02 : 04

DllCall的使用简介已经更新,应该更容易懂了。


RE: DllCall的使用简介 - 博柔派 - 2017-08-07 09 : 21

写得再多,不如一句话一个实例说明。
当然能编就已经不错了。


RE: DllCall的使用简介 - feiyue - 2017-12-16 19 : 06

下面我举个简单的例子吧,请对照前面讲的来理解。
GetUserName是一个WinApi函数,用于获得当前windows登录的用户名。
我们先看看MSDN网站的权威声明:
https://msdn.microsoft.com/en-us/library/windows/desktop/ms724432(v=vs.85).aspx
----------------------------------
BOOL WINAPI GetUserName(
_Out_ LPTSTR lpBuffer,
_Inout_ LPDWORD lpnSize
);
DLL | Advapi32.dll
Unicode and ANSI names | GetUserNameW (Unicode) and GetUserNameA (ANSI)
----------------------------------
从上面的声明我们知道,这个函数位于Advapi32.dll库文件中,
它在库文件中有两个版本名称,分别是GetUserNameW处理Unicode字符串,
GetUserNameA处理ANSI字符串。

我前面说过,WinApi凡是牵涉到字符串的函数,大多都提供了A和W结尾的两个
版本的函数供用户使用,AHK可以根据自身是ANSI版还是Unicode版,在找不到
函数时会智能在函数名末尾添加A或W,从而智能找到刚好匹配的WinApi函数。
所以,AHK调用的第一部分为:DllCall("Advapi32.dll\GetUserName" 就好了。

当然因为我的AHK为Unicode版,我直接使用GetUserNameW也行,AHK能够
直接找到这个函数名,就不会在末尾添加A或W再尝试了。

如果直接使用GetUserNameA行不行呢?由于AHK能够直接找到这个函数名,
同样不会在末尾添加A或W再尝试了。这样该函数在处理输入输出时,内部默认
都是ANSI编码的字符串,所以对于输入字符串,我们需要将AHK原生的Unicode
编码字符串用StrPut转换成ANSI编码字符串,再把字符串首地址传给该函数,
对于输出字符串,需要用StrGet将返回的字符串首地址转换为Unicode字符串。

废话不多说,我们再看看它的参数。它有两个参数,我们一一分析。

第一个参数 “_Out_ LPTSTR lpBuffer”,用于返回Windows登录的用户名字符串。
这个参数是输出的,以LP开头的都是指针,也就是字符串首地址,毫无疑问,
我们用AHK的地址类型“Ptr”作为参数类型是可以的。我前面说过,对于含有STR
的指针,使用“Str”类型会更方便些,下面会详细分析。

第二个参数 “_Inout_ LPDWORD lpnSize”,用于设置处理的字符串长度。
这个参数既是输入一个长度最大值,又是输出结果的长度值,以LP开头的都是指针,
毫无疑问,我们用AHK的地址类型“Ptr”作为参数类型是可以的。但是我前面说过,
对于输出数值的地址参数,使用“*”类型可以自动转换,即输入时,将AHK常规的
字符串型数字,用NumPut写入一个临时内存地址,返回时,从临时内存地址用
NumGet读取数值转换为字符串型数字,供AHK常规使用。所以这个参数用
“Ptr”类型不妥,使用“Int*”则刚刚好。使用无符号的“UInt*”也可以,从临时内存
读取数值时,有符号的Int类型,因为32位二进制中第一位表示正负符号,只有31位
表示数值,所以最大范围为2147483647 (0x7FFFFFFF),而无符号的UInt类型
表示的数值最大范围为4294967295 (0xFFFFFFFF),范围翻了一倍。但是对于
我们这个函数要返回的登录用户名字符串长度而言,长度只有几十字节,所以
没必要用UInt。不过Windows的DWORD类型其实对应于无符号的“UInt”。

参数类型搞清楚了,就可以写出调用格式了:
DllCall("Advapi32.dll\GetUserName", "Str",name, "Int*",size)
最后用消息框显示结果:
MsgBox, % "登录用户名:" name "`n字符串长度:" size
这样就可以了吗?

不行。我前面说过,WinApi都是对内存地址的操作,所以在调用前一般要先用
VarSetCapacity()申请足够的内存,避免WinAPI乱写内存覆盖了有用的数据。
这个函数把返回用户名字符串写入name变量的内存地址,但是我们的AHK的变量
初始都为空的,内部合法占用的内存为0字节,明显不够WinApi写入的。为了可靠
地让WinApi操作合法的内存,我们用VarSetCapacity(name,100)手动申请100
字节的内存给name变量合法占用,因为100字节内存对于ANSI编码的英文可以
写入99个字母(加上末尾的“\0”字符),对于Unicode字符可以写入49个字符,
应该够用了。所以最后完整的调用代码为:
--------------------------------
size:=100, VarSetCapacity(name, size)
DllCall("Advapi32.dll\GetUserName", "Str",name, "Int*",size)
MsgBox, % "登录用户名:`t" name "`n`n字符串长度:`t" size
--------------------------------
这个函数其实有个返回类型“BOOL”,用于返回函数调用是否成功,对应于
AHK的Int类型,由于我们不需要该返回值,所以可以忽略返回类型参数那部分。

最后我们再假设WinApi只有GetUserNameA这一个函数,而我们的AHK又是
Unicode版的情况。对于输入字符串参数,比如 a:="abc",我们只要使用
"AStr",a 即可让AHK帮我们自动转换为临时变量的ANSI字符串传给WinApi。
但是这个函数需要从参数的地址返回字符串,所以不能使用“AStr”类型,需要
手动转换,可以这样做:
--------------------------------
a:="abc"
VarSetCapacity(b, StrLen(a)*2+100) ;-- 6字节不够用
StrPut(a, &b, "CP0")
--------------------------------
这样b变量的内存地址就有了ANSI编码的字符串内容,然后:
--------------------------------
DllCall("Advapi32.dll\GetUserNameA", "Ptr",&b, "Int*",100)
--------------------------------
把b变量的内存首地址传给WinApi,最后读取WinApi的返回结果时
还要把ANSI编码的结果转换回Unicode编码供AHK使用:
--------------------------------
name:=StrGet(&b,"CP0")
MsgBox, % "登录用户名:`t" name
--------------------------------
这样就大功告成了。虽然麻烦点,但是对于WinApi库函数中没有与
AHK本身Unicode还是ANSI版匹配的情况,只好这样手动解决。

由于例子比较简单,没有牵涉到WinApi参数常见的传入数据结构。
其实构造数据结构很简单,先用 VarSetCapacity(a,16) 申请内存,
再用 NumPut(写入数值, a, 偏移字节, "Int") 根据WinApi的数据结构
要求写入数值到正确的偏移位置,然后传入 "Ptr",&a 给WinApi即可。




RE: DllCall的使用简介 - zhanglei1371 - 2017-12-16 20 : 47

感谢feiyue大神的讲解!非常有用!


RE: DllCall的使用简介 - feiyue - 2018-08-16 13 : 12

我对AHK参数类型的等效性有了新的理解,因此使用WinApi更简单了!Smile