DllCall的使用指南

供新手入门和老手参考的教程和相关资料,包括中文帮助

Moderators: tmplinshi, arcticir

feiyue
Posts: 349
Joined: 08 Aug 2014, 04:08

DllCall的使用指南

19 Aug 2018, 19:03


DllCall的使用指南 —— By FeiYue

一、AHK的DllCall简介

1、DllCall是AHK的一个强大功能,可以调用Dll文件中的函数,
例如WinAPI函数,从而可以实现AHK自身没有提供的功能。原则上讲,
可以调用任何WinAPI函数,就可以实现所有编程功能,类似于C语言。

2、DllCall的基础知识请参看AHK帮助,它的使用格式是:
Result := DllCall("[DllFile\]Function" [, Type1,Arg1, Type2,Arg2, "Cdecl ReturnType"])
它与AHK普通函数 Result := Function(Arg1, Arg2) 的区别是:
(1)函数名称部分用 DllCall("[DllFile\]Function" 代替。
(2)参数部分,每一个实际参数前面都附加一个指示参数类型的参数 TypeN。
(3)DllCall的末尾还多出一个参数,指示 调用约定Cdecl 和 返回值类型ReturnType。

3、DllCall的使用牵涉到许多C语言的数据类型,基本上有7种:
char(1字节)、short(2字节)、int(4字节)、
int64(8字节)、float(4字节浮点数)、double(8字节浮点数)。
AHK增加了一个自适应的ptr(地址类型),它匹配操作系统内存地址
所占的字节数,在Win32系统中为4字节,在Win64系统中为8字节。
(注意:32位程序在64位操作系统的兼容模式中运行也视作win32系统)

AHK增加了一个C语言没有的str(字符串类型),专门用于指示字符串,
实际上它相当于ptr的地址类型,只是经过了自动取字符串内存首地址。

AHK增加了一个C语言没有的 *(地址返回类型),专门用于返回数值,
实际上也相当于ptr的地址类型,只是经过了自动传入一个临时内存地址,
然后在DllCall调用完毕后,从临时内存地址读取出数值给参数变量。

4、使用DllCall的一般流程:
(1)先用 VarSetCapacity()、NumPut() 构造数据结构,构造时
要用到那7种C语言的数据类型,&操作符用于取变量的内存首地址。
(2)写出DllCall语句,设定好参数类型和返回类型(如果要返回值),
则ptr、int64、float、double、str、*类型,这几种就有用了。
(3)用 NumGet() 读取数据结构中的数据,必要时对输入或输出的
地址参数用StrPut()、StrGet()转换字符串编码和获取字符串。

二、DllCall调用WinAPI函数的流程

1、AHK程序调用某个Dll文件的Func函数时,第一步先将整个Dll读取
到AHK程序的私有内存中来,如果是 User32.dll, Kernel32.dll,
ComCtl32.dll 或 Gdi32.dll 这些AHK程序本身已经加载的Dll,
就不用再加载了。调用这些Dll中的函数可以不写Dll文件路径。
(注意:32位AHK程序只能加载32位DLL,64位程序加载64位DLL)

2、通过Func这个函数名,和Dll文件加载到内存中的基址,找到内存
中这个函数的机器码入口地址,用于之后的转交控制权。

3、操作系统加载AHK时给它分配了一块专用的内存叫做栈空间,用于程序
暂存和中转数据。对栈的操作CPU有优化的指令,叫作入栈(Push)
和出栈(Pop)。入栈好像将物品放入箩筐的最上层,出栈好像将箩筐
最上层的物品拿走,所以栈的操作是后进先出的,而且不用考虑当前的
箩筐最上层到底堆到多高了。函数的调用基本上是利用栈来传递参数的。

由于CPU的入栈、出栈指令每次操作的数据位数都刚好是操作系统
的内存地址位数,在Win32系统中地址为32位bit(占4字节),在Win64
系统中地址为64位bit(占8字节),所以CPU对栈的操作每次都是
A_PtrSize字节(AHK的A_PtrSize变量在Win32中等于4在Win64中等于8)。
每个传入的参数都会按A_PtrSize字节的整数倍对齐(一倍或二倍)。
(注意:这是编译器为了CPU高效读取数据而共同实行的栈对齐约定)

4、AHK调用函数时,先把各个参数的数值压入栈中,注意每个参数都是
一个内存地址或者是一个少于8字节的数值。内存地址固定占4或8字节,
非内存地址的用于计算的数值最大为8字节。

你可能会问,字符串和数据结构怎么会是一个数值呢,其实字符串
在内存中是一段连续的编码,传入函数的参数是字符串的内存首地址。
同样的,数据结构传入函数的参数也是数据结构的内存首地址。

5、等参数入栈完毕后,程序会跳转到函数的机器码入口地址,把控制权
转交给了函数的机器码手中。函数机器码以当前栈顶的地址作为基址,
偏移N倍个A_PtrSize字节就可以读取父程序压入栈中的各个参数数据。
函数机器码执行完后,再把控制权交还给父程序,并把函数返回值存入
系统的EAX寄存器中供父程序读取,这个寄存器也是A_PtrSize字节的。

三、WinAPI函数的声明类型需要的参数分析(其实就是地址和计算值两种)

1、普通数值。用于计算。
2、字符串。实际是传入字符串的内存首地址。
3、数据结构。实际是传入数据结构的内存首地址。
4、内存地址。可以从那个地址读取数据,也可以写入数据到那个地址。
在MSDN中,参数前面会有_In_与_Out_标识符来区分传入还是传出数据。

四、AHK怎么利用参数类型传递符合WinAPI声明类型的参数

我们经常发现,网上很多WinAPI的调用全部参数类型都用int是可行的,
甚至用在Win64系统上也不出错,原因就是上面的调用函数流程中讲的,
所有的输入参数,在压入栈中时都会按A_PtrSize字节的整数倍对齐。
有哪些是不能用int(或者ptr更合适)来傻瓜化全部设置的呢?

1、对于普通数值,要注意两种情况:
一是输入浮点数,浮点数的构造方式不同于整数,而操作系统默认是整数
入栈,所以输入浮点数要用Float(4字节)或Double(8字节)参数类型。
二是Win32系统中输入8字节的整数。Win32系统的A_PtrSize等于4,所以
输入8字节整数要用int64参数类型。

除了这两种情况,使用char、short、int、ptr都是等效的。

2、对于字符串,如果真的手动传入字符串变量的首地址&a,那么参数类型
任意都行(通常用Ptr),即使用Char类型,也会按A_PtrSize字节对齐。

AHK提供了专门的字符串类型Str,对于变量a作参数,可以内部自动传递其
内存首地址,所以参数不必手动取址了,而且可以用Str,"abc"直接输入
原义字符串,方便了不少。更多的好处,后面再详述。

3、对于数据结构,一般我们要先自己构造a变量的数据结构,再用Ptr,&a传址。
这要用到几个内置的操作指针的函数:先 VarSetCapacity(a,16,0)
申请一定量的内存给变量a,然后 NumPut(1,a,0,"int") 把1写入&a偏移0
字节的位置占4个字节,NumPut(2,a,4,"char") 把2写入&a偏移4字节的
位置占1字节,NumPut(3,a,8,"int64") 把3写入&a偏移8字节的位置
占8个字节。就这样,用7种数据类型构造出WinAPI函数需要的数据结构。
(注意:为了32位和64位可兼容调用,地址要用"ptr"占A_PtrSize字节)

4、对于内存地址,如果是传入数据的,那么用什么数据类型都无所谓。
如果是传出数据的,AHK提供了一个专门的地址返回类型*类型,借助
临时内存地址中转,可以方便地获取参数返回的数值,后面再详述。

五、字符串类型Str的好处

1、首先了解一下字符串在内存中的两种编码:ANSI编码和Unicode编码。
最开始,美国将26个字母、10个数字及英文标点符号进行了ASCII
编码,由于少于256个,所以每个字符占1个字节。后来各国给自己的
语言编码时,文字往往多于256个,所以一个字符占2个字节(如中文),
这样单双混合的编码就是ANSI编码,按单字节识别(以单字节0结束)。
后来国际标准化组织(ISO)推出了包含所有国家文字的Unicode编码,
将英文的每个字符也用双字节编码,按双字节识别(以双字节00结束)。

目前这两种编码都在广泛使用,所以WinAPI凡是带字符串参数的函数,
都基本提供了A和W结尾的两个版本的函数供用户选用。如果选用A结尾
的函数但参数提供的字符串在内存中是Unicode编码,或者选用W结尾的
函数但参数提供的字符串在内存中是ANSI编码,那么显然函数读取输入
字符串会搞错。如果函数按自己的A/W版本确定的编码写入参数的地址,
AHK从参数地址读取字符串时是按AHK的原生编码识别的,如果原生编码
与WinAPI函数的A/W版本不匹配,那么AHK读取返回字符串时会搞错。

2、AHK可以根据自身是ANSI版还是Unicode版智能查找WinAPI函数的版本,
如果AHK是ANSI版,程序使用的原义字符串"abc"就是ANSI编码,当查找
WinAPI函数DeleteFile找不到时,会自动在函数名称后面加A再查找。
如果AHK是Unicode版,当找不到目标函数时,会在名称后面加W再查找。
这样就可以智能匹配了(所以调用的函数名称末尾尽量不要加A/W)。

3、如果WinAPI只有DeleteFileA没有DeleteFileW,而你的AHK是Unicode版
怎么办?一种办法是自己手动转换:先 a:="abc",然后申请一块内存
VarSetCapacity(b,100),然后StrPut(a,&b,"CP0") 转换编码到b变量,
最后用Ptr,&b传递b变量的内存首地址。而利用AStr参数类型可以让AHK
自动将字符串转换为ANSI编码到临时内存地址并把该内存地址传给函数。
另外还有WStr参数类型可以自动转换为Unicode编码,这多么方便呀。
当然如果AStr/WStr参数类型与原生编码一致就不会转换,等效于Str。

AStr和WStr一般用于输入字符串,对于想通过参数变量a返回字符串的,
由于它们可能传入的是临时内存地址,WinAPI函数写入数据也会写入到
临时内存地址中,所以不会写入到参数a的内存地址中来,就没有效果。

4、用Str类型作为输入类型,Str,a 等同于 Ptr,&a,但是对于输出类型,
它还有个好处就是可以更新字符串长度(假设WinAPI函数写入新数据,
改变了字符串长度)。而 Ptr,&a 的方式如果要手动更新字符串长度,
需要用 StrGet(&a) 或者 VarSetCapacity(a,-1),就不太方便。

由于AHK是自动管理内存的,变量占用的内存经常变动,需要增大内存
时就要动态申请内存然后把旧的内容拷贝过去,把变量的地址设到新的
内存地址上,而字符串的内存大小体现在字符串的长度上,所以AHK内部
标记了每个字符串变量的长度。AHK自身对字符串的改变操作,比如
赋值、替换等都会自动调整这个长度标记。而调用WinAPI中的函数,
由于控制权不在AHK手中,发生了什么它也不知道,如果原来的a变量
对应的字符串被改变了,AHK内部没有更新a的长度,可能会造成错误。

六、地址返回类型*类型的好处

我们知道WinAPI除了通过函数返回值返回一个数值外,经常利用内存地址
参数来返回数值,如果我们想用变量a接收返回数值,手动 Ptr,&a 传入a
的内存地址给函数用于写入,那么DllCall调用结束后,我们还要手动使用
b:=NumGet(&a,"int")来读取二进制的数值变为字符串形式的数值。假如
传入a的地址需要一个初值1,那么我们还要手动使用 NumPut(1,a,"int")
将字符串形式的数值写入a的内存地址变为二进制的数值。因为AHK内部
把数值变量也都保存为字符串,所以 a=1 的内存首地址中保存的是1的
字符串编码,即二进制值 Asc("1")==>49,所以必须麻烦的手动转换。

而使用*类型,AHK不直接把参数变量a的内存首地址&a传给WinAPI函数,
而是会传递一个临时内存地址给WinAPI函数(并把a的初值写入该地址),
WinAPI函数把返回数值写入这个临时内存地址,函数返回后,AHK再从
这个临时内存地址读取返回数值到变量a,这样就全自动完成了任务。

七、其他提示

1、对于输入数值,输入的参数都会对齐到A_PtrSize字节,所以参数类型
不重要,U前缀(指示使用无符号的类型)也不重要。但是对于函数的
返回数值(包括DllCall的返回类型和各个*参数类型),它们相当于
AHK内部调用:返回值:=NumGet(返回值的地址,"读取的数据类型"),
所以数据类型和U前缀都很重要,为避免截断应越大越好(可用UPtr)。
有些特大的返回值可能要int64,使用Str作为返回类型可返回字符串。

2、要利用参数变量返回字符串的,应当用 VarSetCapacity(a,256)
申请足够的内存,再 Str,a 传参,避免WinAPI函数乱写非法内存。

3、调用约定"Cdecl"仅用于C语言写的函数,WinAPI基本上都是标准调用。

4、最后再提示一下根据WinAPI声明类型设定AHK参数类型的方法:
先把所有参数类型都设为ptr(对于DWORD类型用uint也可),然后
看WinAPI的声明类型为 Long64 或 long long 的采用int64,然后
看WinAPI的声明类型为浮点类型的选用 Float 和 Double,然后
看WinAPI的声明类型为包含Str的选用 Str / AStr / WStr,然后
看WinAPI的声明类型为_out_ LP开头的采用Ptr*,就搞定了!
(注意:WinAPI的难点其实在构造数据结构,ptr字节计算)

Return to “教程资料”

Who is online

Users browsing this forum: No registered users and 15 guests