Autoahotkey关于dllcall()函数最全⾯的解释
这是⼀篇很长的技术⽂章,需要极强的耐⼼才能读完,如果想理解得读两边!
DllCall是AHK的⼀个强⼤功能,⽤来调⽤Dll****⽂件中的函数。
⽤法格式如下:
Result := DllCall("DllFile\Function" , Type1, Arg1, Type2, Arg2, "Cdecl ReturnType")
许多新⼿⼀看这⼀长串,倒吸⼀⼝冷⽓,这是啥玩意!可能觉得DllCall很复杂、很难⽤,于是对Windows强⼤的WinAPI函数就不敢上⼿,只能羡慕那些⼤神们调⽤WinAPI实现各种奇妙功能。其实DllCall的使⽤并不难,下⾯我就为⼤家拨开DllCall的神秘⾯纱,其实你也能简单学会!
惊吓_small.jpg
⼀、dll是啥?
DLL(Dynamic Link Library)⽂件为动态链接库⽂件,⼜称“应⽤程序拓展”,是软件⽂件类型。在Windows中,许多应⽤程序并不是⼀个完整的可执⾏⽂件,它们被分割成⼀些相对独⽴的动态链接库,即DLL⽂件,放置于系统中。当我们执⾏某⼀个程序时,相应的DLL⽂件就会被调⽤。⼀个应⽤程序可使⽤多个DLL⽂件,⼀个DLL⽂件也可能被不同的应⽤程序使⽤,这样的DLL⽂件被称为共享DLL⽂件。
DLL⽂件中存放的是各类程序的函数(⼦过程)实现过程,当程序需要调⽤函数时需要先载⼊DLL,然后取得函数的地址,最后进⾏调⽤。使⽤DLL ⽂件的好处是程序不需要在运⾏之初加载所有代码,只有在程序需要某个函数的时候才从DLL中取出。另外,使⽤DLL⽂件还可以减⼩程序的体积。
组成⼀个软件的⽂件中.dll占据相当多⼀部分。
通过使⽤ DLL,程序可以实现模块化,由相对独⽴的组件组成。 例如,⼀个计帐程序可以按模块来销售。 可以在运⾏时将各个模块加载到主程序中(如果安装了相应模块)。 因为模块是彼此独⽴的,所以程序的加载速度更快,⽽且模块只在相应的功能被请求时才加载。
注:为了节约栈的空间,Api设定每个参数时,不都是占⽤4字节(32位操作系统的地址指针⼤⼩)或8字节(64位操作系统的地址指针⼤⼩),⽽是Char类型1字节,Short类型2字节,Int和Float类型4字节,Int64和Double类型8字节,Ptr类型(即地址指针类型)视操作系统为32位还是64位⾃动为4字节或8字节(AHK提供此类型可以⾃适应操作系统的地址指针⼤⼩)。
Api函数运⾏时,把栈上的各参数按栈顶加偏移(字节数)来读取各个数据,AHK如果不按Api函数声明的约定字节数来传递各个参数,如果有⼀个参数的类型错误(字节数不对),Api读取后⾯参数的偏移就会出错。
三、****AHK****参数类型的分类说明:(以调⽤****WinApi****函数为代表)
1、传递给WinApi的参数其实只有两类:简单数值和内存地址。前者包含:int、int64、float、double等,后者包含:char(charp同义)、ptr、str三种。⼀般与WinApi函数的声明格式对应,也可以不⼀致但字节数⼀致就⾏了。⽐如char、ptr、str类型都是传址,AHK会智能根据⾃⾝是32位版的还是64位版的将对应的值压⼊栈中占4字节或8字节。如果我们已经知道AHK是32位版的,⽤int代替ptr也是可以的,因为int也是把数值压⼊栈中占4字节。只要确保每个参数的字节数匹配WinApi函数声明的约定字节数就⾏。
2、如果WinApi需要传⼊字符串怎么办呢?没什么问题,字符串在内存中是连续的编码数值,只要传
⼊它的起始地址就⾏了,如果有另外的参数传⼊了长度,则可以准确处理这个长度的字符串,如果没有另外传⼊长度,则以字符串的默认结束符来处理字符串(Ansi编码的字符串以1个字节值0结束,Unicode编码的字符串以2个字节值0结束)。AHK为输⼊字符串设置了str类型,后⾯的参数可以是原义的字符串⽐如"OK",AHK会⾃动把这个字符串保存到临时变量a,然后把变量的地址(&a)压⼊栈中。如果后⾯的值是⼀个变量b,则省了临时变量,直接把地址(&b)压⼊栈中,此时它的输⼊功能与ptr,&b是⼀样的。但是与ptr形式的区别是它可以更新后⾯的变量(输出功能不同),即更新字符串长度VarSetCapacity(b,-1)。因为AHK内部对⼤多数变量都视为字符串,并标记了变量长度,AHK对变量的操作都⾃动维护这个长度标记(⽅便⾃动扩充内存),⽽WinApi如果内部操作改变了b变量的内容,⽐如"OK"改为了"OK2",由于脱离了AHK的操作,AHK内部还是视为它的长度为
2(实际为3)所以使⽤b变量时⽐如 MsgBox, %b%或a:=b就会出错。ptr不会更新⽽str会更新b的长度。由于字符串有Ansi编码,也有Unicode编码,前者字母、数字和英⽂标点符号都是占1个字节,汉字占2个字节,⽽后者所有字符都占两个字节,WinApi要处理这两种可能的情形怎么办呢?微软的⽅法是绝⼤多数WinApi都提供两个版本(分别以A或W结尾),⽅便使⽤者调⽤合适的⼀版。AHK的原⽣字符串(⽐如
a:="OK")是根据AHK是Ansi版还是Unicode版分别是Ansi编码或Unicode编码,那么⽤ptr,&a的⽅式传⼊字符串a的内存地址时,怎么确保WinApi刚好需要的是AHK原⽣编码呢?没问题,AHK会智能根据
⾃⼰是Ansi版还是Unicode版⾃动在调⽤函数后⾯加A或W,这样就刚好了。但是如果WinApi的某个函数只有Ansi版,⽽AHK原⽣编码为Unicode版,显然ptr的输⼊形式和str的输⼊形式都会出错,因为它们都使⽤原⽣编码,这时可以⽤AHK提供的Astr类型和Wstr类型,这两种类型明确指⽰了要提供给函数的字符串使⽤的编码,如果指定的Astr与原⽣编码不⼀致,则会利⽤⼀个临时变量b,将字符串⽤StrPut转换编码到b变量中,然后把这个临时变量的地址(&b)压⼊栈中。当然如果指定的Astr与原⽣编码⼀致则不⽤转换,直接把变量地址压⼊栈。注意Astr和Wstr可能传⼊的是临时变量的地址,如果需要返回字符串,WinApi修改的也可能是临时地址中的内容,不能体现在参数的变量中来,所以这时要⽤ptr或者str类型,必要时⼿动StrPut转换输⼊需要的编码。如果WinApi修改返回的编码为Ansi编码,不是当前AHK的原⽣编码时,这时⾃⼰⼿动转码 StrGet(&a,"CP0") 即可。
利⽤StrPut可以将原⽣编码(由AHK是Ansi版还是Unicode版决定的)的字符串转换为⽬标编码,StrGet读取⽬标编码转换为当前的原⽣编码。
3、WinApi返回数值⼀般有两种形式,⼀种是函数返回值,通过寄存器返回,DllCall读取寄存器的数值到函数返回变量,这种只能返回1个值(这时由返回类型指定读取的字节数,返回类型不对可能结果不同)。另⼀种是WinApi把某个数值保存到某个内存地址中并占⼏个字节(⽐如占1个字节对应char类型,4字节对应int类型,8字节对应int64类型),AHK通过char(charp同义)传递⼀个临时内存地址给函数,函数把数值写⼊这个临时内存地址,函数返回后,AHK从这个临时内存地址读取1个字节的数
值 NumGet(临时地址值,"char") ,这样就实现了通过传址参数来返回多个数值结果。虽然类型⼀般⽤于返回值,但如果这个char,a后⾯的a值也要作为输⼊值对WinApi有⽤,AHK会在临时内存地址中⽤
NumPut(a,临时地址值,"char") 把a的值存⼊这个地址,注意char限定了仅写⼊1个字节的数值,范围为-128~127,超出1个字节的部分会舍去。
4、ptr类型只是简单传递了变量的内存地址给函数,它没有str、*类型那么多内部智能转换操作,它主要⽤于传递⼀个数据结构给函数。函数的参数往往需要特定的数据结构,因为只要得到这个结构的⾸地址,按照这个结构的约定格式,就能⽤⾸地址加偏移获取各部分的数据了。们⼀般先⽤VarSetCapacity(a,100) 申请⼀块内存,然后利⽤NumPut 按WinApi约定的格式⼿动把数值写⼊a变量内存中相应的地址,数据结构设定好后,再把&a地址传⼊函数,调⽤结束后,还可以⼿动⽤NumGet 从a变量的数据结构中读取需要的值。
Api的读写都是对内存地址的操作,所以带⼤量返回的参数⼀般要先⽤VarSetCapacity申请⼀块⾜够的内存,避免乱写内存覆盖了有⽤的数据。
四、为什么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"调⽤约定是栈的平衡由调⽤者来完成,调⽤者压⼊了多个参数到栈中,最后栈顶指针的恢复要由调⽤者来做。⽽标准调⽤则要函数⾃parameter数据类型
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论