这是一篇很长的技术文章,需要极强的耐心才能读完,如果想理解得读两边!
DllCall是AHK的一个强大功能,用来调用Dll****文件中的函数。
用法格式如下:
Result := DllCall("DllFile\Function" , Type1, Arg1, Type2, Arg2, "Cdecl ReturnType")
许多新手一看这一长串,倒吸一口冷气,这是啥玩意!可能觉得DllCall很复杂、很难用,于是对Windows强大的WinAPI函数就不敢上手,只能羡慕那些大神们调用WinAPI实现各种奇妙功能。其实DllCall的使用并不难,下面我就为大家拨开DllCall的神秘面纱,其实你也能简单学会!
一、dll是啥?
DLL(Dynamic Link Library)文件为动态链接库文件,又称“应用程序拓展”,是软件文件类型。在Windows中,许多应用程序并不是一个完整的可执行文件,它们被分割成一些相对独立的动态链接库,即DLL文件,放置于系统中。当我们执行某一个程序时,相应的DLL文件就会被调用。一个应用程序可使用多个DLL文件,一个DLL文件也可能被不同的应用程序使用,这样的DLL文件被称为共享DLL文件。
DLL文件中存放的是各类程序的函数(子过程)实现过程,当程序需要调用函数时需要先载入DLL,然后取得函数的地址,最后进行调用。使用DLL文件的好处是程序不需要在运行之初加载所有代码,只有在程序需要某个函数的时候才从DLL中取出。另外,使用DLL文件还可以减小程序的体积。
组成一个软件的文件中.dll占据相当多一部分。
通过使用 DLL,程序可以实现模块化,由相对独立的组件组成。 例如,一个计帐程序可以按模块来销售。 可以在运行时将各个模块加载到主程序中(如果安装了相应模块)。 因为模块是彼此独立的,所以程序的加载速度更快,而且模块只在相应的功能被请求时才加载。
此外,可以更为容易地将更新应用于各个模块,而不会影响该程序的其他部分。 例如,您可能具有一个工资计算程序,而税率每年都会更改。 当这些更改被隔离到 DLL 中以后,您无需重新生成或安装整个程序就可以应用更新。
(下面都以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读取目标编码转换为当前的原生编码。
第二、三步看帮助文件就很容易懂了,第一步我慢慢讲解,引导大家深入理解参数类型。
与AHK普通函数的区别在于每个参数要根据调用函数的声明添加对应的参数类型和设定函数返回类型(这两个是重点)。
二、为什么每个参数前面都要设定准确的参数类型?
1、DllCall调用的大概流程是:DllCall首先把Dll文件整个读取到AHK进程的私有内存中,然后通过函数名字符串找到对应的函数入口地址,然后把参数一个一个压入AHK进程的栈中,然后跳转到函数的入口地址,控制权交给了入口地址的机器码手中。机器码会从栈中把参数的数值一个一个读取回来,然后执行自身的代码。机器码执行完毕后,会把返回值写入通用寄存器EAX中,然后把控制权归还给AHK,然后AHK从通用寄存器EAX中读取返回值。
2、我们知道,AHK普通函数是动态解析的,所以各个函数参数都用逗号作为分隔符隔开,不用考虑参数的数据类型(实际上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程序崩溃)
注:为了节约栈的空间,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"调用约定是栈的平衡由调用者来完成,调用者压入了多个参数到栈中,最后栈顶指针的恢复要由调用者来做。而标准调用则要函数自己来恢复,掉用者只管压栈不管恢复。所以如果调用C函数不加上"Cdecl",默认使用标准调用,栈的平衡无法完成,多次调用后会耗尽栈资源。
2、函数返回类型不是很重要,对Api运行没有影响,只对AHK读取函数返回值有影响(使用 NumGet 读取)。比较特殊的是str返回类型,AHK会把返回的数值看做字符串首地址,并复制字符串到返回变量中(使用 StrGet 读取)。
3、U前缀指示了使用无符号的类型,这对于输入数值没有意义(int64除外),因为输入时指定char和uchar,NumPut写入内存的都是同样的数值,但对于Api 的输出数值,即 函数返回类型 和 char*类型 就有意义了,AHK内部调用NumGet读取的值可能不同。
4、Windows数据类型对应于AHK参数类型的简单判断:(懂了上面的就不难了)简单数值的WinApi参数绝大部分对应int类型,比如:DWORD、LONG、BOOL、COLORREF。浮点类型对应float、double。带64的、LONGLONG对应int64。比较特殊的是SIZE_T类型,它在32位系统中对应int,在64位系统中对应int64,所以要用AHK自适应的ptr类型来对应。内存地址的WinApi参数对应AHK的三种形式,一般WinApi声明中的各种句柄(H开头的)、带LP或P开头的、带PTR的都是指针,即地址类型,一般对应ptr类型。但是带STR的指针则对应str类型更方便些。如果是用于输出结果的指针就对应*类型。对于输入类型U前缀不重要,因此uint写成int也没问题。
内存地址的WinApi参数对应于AHK的三种形式:一般WinApi声明中的各种句柄(H开头的)、带LP或P开头的、带PTR的都是指针,即地址类型,一般对应Ptr类型。但是带STR的指针则对应Str类型更方便些(用Ptr就稍麻烦,参看上面的说明)。
如果是用于输出结果的指针就对应*类型(用Ptr就不可取,参看上面的说明)。
九、举个简单的例子
请对照前面讲的来理解。GetUserName是一个WinApi函数,用于获得当前windows登录的用户名。
我们先看看MSDN网站的权威声明:
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)
从上面的声明我们知道,这个函数位于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即可。