发表回复 
DllCall的使用简介
2016-06-07, 01 : 31 (这个帖子最后修改于: 2017-07-11 15 : 03 by feiyue.)
DllCall的使用简介
;--------------------------------
; DllCall的使用简介 By FeiYue
;--------------------------------

DllCall是AHK的一个强大功能,用来调用Dll文件中的函数。用法格式如下:
Result := DllCall("[DllFile\]Function" [, Type1,Arg1, Type2,Arg2, "Cdecl ReturnType"])

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

我们都用过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()分配内存,及用NumGet()、NumPut()读写内存数据。
第三步要学会用StrGet()、StrPut()转换字符串编码。

利用StrPut可以将原生编码(由AHK是ANSI版还是Unicode版决定的)
的字符串转换为目标编码,StrGet读取目标编码转换为当前的原生编码。

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

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

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

2、我们知道,AHK的脚本是动态解析的,函数参数都用逗号作为分隔符隔开,不用考虑参数
的数据类型,而调用Dll中的函数,要把参数的数值连续压入栈中,中间可是没有分隔符的。

我要特别强调的是,所有传给机器码的各个参数都是一个数值,字符串是把字符串在内存中
的首地址压入栈中,数据结构也是把内存首地址压入栈中,简单数字和地址值就更是数值了。

如果所有机器码对应的WinAPI函数,参数都用固定的数值类型,比如4字节的整型Int来接收,
那么这样约定好后,AHK传递参数时就不需要说明参数类型了,每个参数都压入栈中占用
4个字节就好了。但是WinAPI的参数很多都是内存地址,比如字符串是字符串的内存首地址,
数据结构(比如常见的句柄)也是内存首地址等等,而内存地址在Win32位系统中是占4字节,
在Win64位系统中是占8字节。WinAPI的参数也有一些是简单数值,比如长、宽、颜色值等等,
假如无脑全部约定Win32位系统的参数都占4字节,Win64系统的参数都占8字节,确实方便了
其他人调用,但是却浪费了栈的空间,而且如果Win32位系统需要调用或者返回一个8字节
的整型Int64参数,那么也会打破这种无脑的约定。所以编写WinAPI函数的程序员不会迁就
调用者来个死板约定,该用地址型就地址型(这在AHK中对应Ptr类型,会根据AHK自身是
32位版还是64位版自动为4字节或8字节),该用整型就整型(这在AHK中对应Int占用4字节),
那么调用者就要迁就函数编写者的声明了,函数编写者发布函数时,都会声明每个参数的
数据类型(表面上五花八门,实际上基本上就是地址型和整型两种),所以DllCall调用时,
就要按照WinAPI函数的声明,在每个参数前面加上合适的参数类型,参数的数值压入栈中时,
该占8字节就占8字节,该占4字节就占4字节,这样WinAPI函数读取的时候根据函数声明来
读取就不会出错了。(如果参数类型错误,机器码读到非法的值会让AHK程序崩溃)

二、为什么AHK的参数类型不只用Ptr和Int两种?

我前面说过,WinAPI函数的参数数据类型,表面上五花八门,实际上基本上就是地址型
Ptr(视操作系统自动为4或8字节)和整型Int(4字节)两种,因为这是编程中表示数值的
最常用类型,如果有哪个奇葩的程序员为了节约一点点栈空间,在传入简单数值时,用到了
16位整型Short(2字节)和8位整型Char(1字节),那我真是服了他了,这是非常罕见的。

因此,我们不是可以用Ptr和Int走遍天下了?先看看WinAPI的参数类型声明,然后简单判断
一下是地址还是简单数值,简单数值有个特殊的SIZE_T类型是为了输入可变类型的数值的,
在Win32位系统为4字节,在Win64系统为8字节,所以我们记住把它设为自适应的Ptr类型,
其他的简单数值,除了Int64、Long Long、Double这些明确的8字节类型我们用AHK的Int64
类型以外,其他的都用4字节的Int类型就基本上不会错了。(浮点数即小数,比较特殊,应当
使用Double类型表示8字节,用Float类型表示4字节,这种在函数的声明中很容易判断)

于是前面调用Dll文件中的InStr函数的例子写成下面的参数类型也不错:
Pos:=DllCall("Dll文件\InStr", "Ptr",&(s1:="abc123"), "Ptr",&(s2:="abc"), "Int")
为什么AHK还要更多地设立Str类型和*类型呢?因为Ptr和Int两种类型只是死板地传递数值,
没有多余动作,而Str类型和*类型都有神奇的调用前后内部转换操作,且听我一一道来。

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

1、首先我们要认识到字符串在内存中是以什么形式存在的。
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类型(不转换)就行了。用它的
好处,一是可以直接采用字符串(比如"Str","abc123"),对于变量也不用取地址&。
另一个更重要的好处是,调用结束后,会更新对应变量的字符串长度。

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

注1:Ptr形式可以用VarSetCapacity(a,-1)或者StrGet(&a)两种方式手动更新长度。
注2:Astr和Wstr可能传入的是临时变量的地址,如果需要返回字符串,WinApi修改
的也可能是临时地址中的内容,不能体现在参数的变量所在的内存地址中来,所以
如果需要返回字符串,还是要用Ptr或者Str类型,因为这两种类型,压入栈中的地址
就是变量的内存首地址(没有经过任何转换,必要时需要手动转换成正确的编码)。
如果WinApi返回的字符串编码与AHK原生编码不同时,需要自己手动用StrGet()转码。

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

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

2、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个字节的数值。

3、用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、调用约定:C语言写的函数,返回类型前一般要添加"Cdecl",而WinApi
使用标准调用形式则不用添加。若C函数编译时指定了使用标准调用也不用。

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

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

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

简单数值的WinAPI参数绝大部分对应Int类型,比如:DWORD、LONG、BOOL、
COLORREF。带64的、LONGLONG对应Int64。浮点类型推荐对应Float、Double。
比较特殊的是 SIZE_T 可变数据类型,它要用AHK自适应的Ptr类型来对应。
对于输入类型U前缀不重要,因此UInt写成Int也没关系。

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

查找这个用户的全部帖子
表示感谢 引用并回复 移动视图置页面顶端
[+] 1用户表示感谢feiyue
2017-07-11, 02 : 04
RE: DllCall的使用简介
DllCall的使用简介已经更新,应该更容易懂了。
查找这个用户的全部帖子
表示感谢 引用并回复 移动视图置页面顶端
[+] 1用户表示感谢feiyue
发表回复 


论坛跳转:


联系我们 | Autohotkey 中文站 | 回到顶部 | 回到正文区 | 精简(归档)模式 | RSS 聚合