转自:卢光庆
于 2022-08-20 09:14:00 修改
————————————————
版权声明:本文为CSDN博主「卢光庆」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/sdlgq/article/details/112465234
API调用解析
1、静态数据类型参数
1、静态数据类型(number、pointer等)在API调用时,不管声明或不声明,都是传值,只能作为输入参数。
2、如果要作为“输出参数”传递指针(传址),需要使用结构体代替。
3、使用table定义结构体struct,在结构体中定义静态类型。
4、struct中所有成员应当赋于初始值(待验证,看下面的小测试)。
如:
数值的指针: {int abc} 或 {int abc=123}。
数值数组的指针:{int data[4]} 。
字节数组的指针:{byte data[4]} 或 raw.buffer()
// 通常,我们使用class来定义API函数中需要用到的结构体:
class POINT{
int x = 0; // 结构体成员必须设定初始值
int y = 0;
}
// 创建一个结构体对象
pt = POINT();
// 每个结构体会创建一个_struct只读字段记录类型定义,语义如下:
pt = { _struct = "int x;int y" }
小测试: 关于struct中所有成员应当赋于初始值
测试结论:即便没有给struct的成员赋予初始值,也一切OK。
我觉得像 int 这种定长的数据类型,没有初始值也没关系。
如果是数组,应该要提前确定结构长度。
易语言dll代码:
![](https://aar.chengxu.online/upload/files/20230728/1690534189.png) 编辑
aardio调用代码:
![](https://aar.chengxu.online/upload/files/20230728/1690536593.png)
没有赋予struct初始值,int类型的值默认为0
![](https://aar.chengxu.online/upload/files/20230728/1690536647.png)
传递给api后,也可以正常修改变量值。
|
2、动态数据类型参数
1、动态类型(string、buffer)本身就是指针,所以不能再传址。
也就是说,从表面上来看,他们只能作为输入参数,不能作为输出参数。
然而,虽然如此,但因其本身就是指针,所以其值在api中也可以被修改。
2、struct在声明API时,可以作为输入或输出参数;不声明API时,强制作为输出参数。
作为输入参数时,传递的是数值(传值),不作为API返回值返回。
作为输出参数时,传递的是指针(传址),并作为API返回值返回。
3、不声明直接调用API:
一、参数:
1、不声明直接调用API,可以根据需要灵活改变参数类型,也更方便、节省资源,建议使用。
2、调用约定在加载DLL的参数中指定,支持cdecl不定个数参数。
3、null参数不可省略。
4、数值参数一律处理为32位int整型。(小于32位的整数、枚举类型、8位或32位bool值都跟int 32位数值兼容)
5、64位整数可以用math.size64对象表示。(或者用两个数值参数表示一个64位整数值参数,其中第一个参数表示低32位数值,第二个参数表示高32位数值)
6、数值类型的指针(输出参数)一律使用结构体表示。
7、数组指针,使用结构体指针替代,如 {int data[4]} 。
8、结构体,一律处理为输出参数,并在aardio返回值中返回。
9、除结构体外的其他类型,只能作为输入参数。
0、注意在aardio中,任何结构体在API调用中传递的都是结构体指针(传址)(待进一步验证,因为在下面的简单数值型struct测试代码中,进行不传址测试时,api中并未能成功改变真实变量的值。
这里说的意思很有可能是:任何结构体在API直接调用中传递的都是结构体指针)。
二、返回值:
1、直接调用API的,返回值默认为int类型。
2、可以使用 【API尾标】 改变返回值为其他类型。
3、未声明的API函数,只是一个普通的aardio函数对象,不能作为函数指针参数传给API参数(声明后的API函数对象是可以的)。
4、结构体总是作为输出参数,附加在api返回值后面返回。
三、API尾标:
1、当不声明直接调用API时,API函数名尾部如果不是大写字符,则可以使用一个大写的特定字符(这就是API尾标)修改默认的API调用规则。
2、在API函数名后添加尾标,不会影响到查找API函数的结果,无论真实的API带不带指定的尾标,aardio都能找到真实的函数。
3、所有可用的 [API尾标] 及代表的规则如下:
- dll.ApiNameW() 切换到Unicode版本,字符串UTF8-UTF16双向转换
- dll.ApiNameA() 切换到ANSI版本,字符串不作任何转换
- dll.ApiNameL() 返回值为64位LONG类型
- dll.ApiNameP() 返回值为pointer指针类型
- dll.ApiNameD() 返回值为double浮点数
- dll.ApiNameF() 返回值为float浮点数
- dll.ApiNameB() 返回值为byte类型(C++中的8位bool)
4、对于已存在W尾标的API函数,可在调用时使用其他尾标,并且仍然可以正确检测并切换到API函数的Unicode版本。
四、使用字符串
1、字符串、buffer类型字节数组,一般作为字符串指针使用。
2、如果API需要向字符串指向的内存中写入数据,那么必须使用raw.buffer()函数创建定长的字节数组。
3、普通的aardio字符串指向的内存是禁止写入的(这种说法,仅对aardio常规操作而言,看看下面的api测试代码,可以成功修改字符串的值),aardio中修改普通字符串会返回新的字符串对象,而不是在原内存上修改数据。
4、对于Ansi版本的API,string字符串直接输入原始的数据(文本默认是UTF8编码)。
对于Unicode版本的API,string字符串会被强制转换为Unicode(UTF16)。
buffer类型的参数,总是以二进制方式使用原始数据与API交互(不会做文本编码转换)。
五、Ansi和Unicode
1、可以在 raw.loadDll ( ) 加载DLL时,可以在调用约定中添加 ",unicode",让它默认使用Unicode API。
2、可以在函数名后添加尾标"A"或“W”,声明Ansi或Unicode版本的API。
如果同时指定尾标版本 和 loadDll约定版本,则以尾标版本为准。
aardio在找不到该版本的API函数时,会移除尾标,但依然会根据指定的尾标版本,进行字符串编码。
4、一些API在接收字符串、字节数组等参数时,通常下一个参数需要指定内存长度, aardio中用#操作符取字符串、缓冲区的长度时,返回的都是字节长度,一些API可能需要你传入字符个数,如果是Unicode版本的API一个字符为两个字节,对于一个UTF8字符串应当事用string.len()函数得到真正的字符长度, 而Unicode字符串则用#取到字节长度后乘以2即可。
简单数值型struct测试
易语言编写的dll代码:
![](https://aar.chengxu.online/upload/files/20230728/1690534190.png)
编辑
aardio调用代码:
1、定义为输入参数时【不传址】:
// 结构体未定义为输出参数:
import console;
testdll = raw.loadDll("C:\Users\Administrator\Desktop\test.dll")
test=testdll.api("test","int(struct)","stdcall")
var i = {int abc=123}
var a,b = test (i)
console.dump("a:",a)
console.dump("b:",b)
console.dump("i:",i)
console.pause(true);
执行结果
![](https://aar.chengxu.online/upload/files/20230728/1690534191.png)
编辑
![](https://aar.chengxu.online/upload/files/20230728/1690534193.png)
编辑
执行结果:传递进去数值正常;修改结构体数据失败;返回值正常;结构体未作为返回值返回;
2、定义为输出参数时【传址】:
import console;
testdll = raw.loadDll("C:\Users\Administrator\Desktop\test.dll")
test=testdll.api("test","int(struct&)","stdcall")
var i = {int abc=123}
var a,b = test (i)
console.dump("a:",a)
console.dump("b:",b)
console.dump("i:",i)
console.pause(true);
执行结果:
![](https://aar.chengxu.online/upload/files/20230728/1690534194.png)
编辑
![](https://aar.chengxu.online/upload/files/20230728/1690534195.png)
编辑
执行结果:传入api数值正常;api中修改结构体变量数据正常;返回值正常;结构体作为返回值返回成功;
3、直接调用API函数时:
import console;
testdll = raw.loadDll("C:\Users\Administrator\Desktop\test.dll")
var i = {int abc=123}
var a,b = testdll.test(i)
console.dump("a:",a)
console.dump("b:",b)
console.dump("i:",i)
console.pause(true);
结果同上。因为struct被强制作为输出参数进行了传址。
简单string字符串传址测试
1、易语言编写的dll代码:
![](https://aar.chengxu.online/upload/files/20230728/1690534196.png)
编辑
2、aardio调用示例(传址):
import console;
testdll = raw.loadDll("C:\Users\Administrator\Desktop\test.dll")
test=testdll.api("test","string(string&)","stdcall")
var i =..string.fromto("张三丰")
var a,b = test(i)
console.dump("a:",a)
console.dump("b:",b)
console.dump("i:",i)
console.pause(true);
3、执行结果:
![](https://aar.chengxu.online/upload/files/20230728/1690536755.png)
![](https://aar.chengxu.online/upload/files/20230728/1690536772.png)
执行结果:传入api文本正常;api中修改文本变量数据失败;返回值正常;传出参数修改后的数据作为返回值返回。
4、aardio调用示例(不传址):
![](https://aar.chengxu.online/upload/files/20230728/1690535676.png)
执行结果:传入api文本正常;api中修改文本变量数据成功;返回值正常;无附加返回值。
我的大胆猜想
1、可能是出于保护内存数据安全或其他目的,aardio对除结构体以外的数据类型,都做了保护,禁止传址,防止其数据被非法篡改。
2、当这些数据类型被强制传址时,aardio会申请与其数据一致的临时变量,传址给api执行,并获得api执行结果和被修改后的临时变量数据,返回给aardio。
3、所谓的“只读”、“不可修改”只是相对而言,因为你无法提供准确有效的获取 诸如 var i=123 这种变量的地址的有效方法,所以才感觉无法修改。
4、为了验证这个猜想,做一个lstrcpyn测试:
通过测试结果可以看出,api确实成功修改并返回了传址进去的数值型变量的值,但实际上真实变量的值却并没有发生改变。因为api改的是临时变量的值,aardio取的也是临时变量的值。
5、通过lstrcpyn测试和上面的文本传址测试,其结果初步符合上述猜想。
注:该猜想已于2021-10-12版本 v33.18.3 更新 raw.argsPointer 库时提供的例程:
范例程序\ aardio 语言\ 语言扩展\ 结构体二级指针
中得到了证实,作者做了如下注解:
aardio 结构体作为调用 API 的参数时会分配一块临时的内存,
并将 aardio 结构体的值复制过去,然后将该内存的指针作为调用 API 的参数,
在调用 API 结束后再将内存中新的值同步到 aardio 结构体,然后立即释放临时内存,
释放临时内存是立即操作,而非等待垃圾回收器操作。
文本传址与不传址的深入测试:
同样用易语言写一个dll,功能:
1、显示传递进来的文本变量的值
2、显示传递进来的文本变量的地址
3、修改传递进来的文本变量的内存数据
4、返回一个文本值
![](https://aar.chengxu.online/upload/files/20230728/1690535807.png)
1、用两个内容【一样】的string变量进行【不传址】测试。
![](https://aar.chengxu.online/upload/files/20230728/1690535852.png)
第一步:传递第1个变量 t1,api中提示:内容为“张三丰”,变量地址为 36657216
![](https://aar.chengxu.online/upload/files/20230728/1690535881.png)
第二步:api中修改了变量 t1 的内容为“我很好”,aardio中console也同步显示,返回值为“哈哈哈”,t1为“我很好”,OK,正确无误。
第三步:传递第2个变量 t2,api中提示:内容为“我很好”,变量地址为 36657216。
那么问题来了,为什么 t2 不是 “张三丰” 呢?
那是因为aardio中,同样内容的文本变量,共享同一内存地址。
你改了一个,等于是改了所有。
这也是为什么这两个变量传递进去的“变量地址”是一样的原因。
所以,这里传给api的,就是string变量的“真实地址”。
![](https://aar.chengxu.online/upload/files/20230728/1690535904.png)
第四步:执行完毕,毫无悬念。
结论就是:t1 、t2 内容一样,地址一样,api改一个变量的内存数据,等于全改了。“不传址”的情况下,传给api的反而是真实地址。
2、用两个内容【不一样】的string变量进行【不传址】测试。
![](https://aar.chengxu.online/upload/files/20230728/1690535927.png)
第一步:传递第1个变量 t1,api中提示:内容为“张三丰”,变量地址为 27875392
![](https://aar.chengxu.online/upload/files/20230728/1690535961.png)
第二步:api中修改了变量 t1 的内容为“我很好”,aardio中console也同步显示,返回值为“哈哈哈”,t1为“我很好”,OK,正确无误。
第三步:传递第2个变量 t2,api中提示:内容为“张无忌”,变量地址为 27875424。
![](https://aar.chengxu.online/upload/files/20230728/1690535977.png)
第四步:执行完毕,变量 t2 内容变为“我很好”。
执行完毕,结论就是:t1 、t2 内容不一样,地址也不一样,“不传址”的情况下,传给api的都是真实地址,都被api成功修改。
3、用两个内容【一样】的string变量进行【传址】测试。
因为string传址会被aardio作为返回值返回,也就是说api会有多个返回值,所以我们下面的测试代码中,将api的执行结果放在console.log的最后一个参数,以便于能全部显示出来。
第一步:传递第1个变量 t1,api中提示:内容为“张三丰”,变量地址为27423792。
![](https://aar.chengxu.online/upload/files/20230728/1690536074.png)
第二步:api中没有成功修改变量 t1 的内容为“我很好”,t1 的真实内容仍然为“张三丰”。返回值为“哈哈哈”,但返回值中的 t1 为“我很好”。
第三步:传递第2个变量 t2,api中提示:内容为“张三丰”,变量地址为 27423792。
同样,t1、t2 内容一样,地址一样,因为 t1 没有被修改,所以 t2 传进去,还是 “张三丰”。
所以,这里传给api的,应该不是string变量 t1 的“真实地址”。
但 api 却成功修改并返回了修改后的“正确”结果,所以猜想这里用了一个临时变量去“欺骗”api,而非真实的 t1。
而且,两次传递的临时变量地址是一样的,但第一次传递后,明明内容被修改了,第二次传递却又恢复了。
这说明:aardio将临时变量传递给api时,是先给临时变量同步了真实变量的值。
第四步:执行完毕,结论就是:“传址”的情况下,先把真实变量的值,复制给临时变量,再把临时变量传给api。api改的是临时变量的值,返回的也是临时变量的值。
4、用两个内容【不一样】的string变量进行【传址】测试。
![](https://aar.chengxu.online/upload/files/20230728/1690536121.png)
第一步:传递第1个变量 t1,api中提示:内容为“张三丰”,变量地址为36533296。
![](https://aar.chengxu.online/upload/files/20230728/1690536149.png)
第二步:api中没有成功修改变量 t1 的内容为“我很好”,t1 的真实内容仍然为“张三丰”。返回值为“哈哈哈”,但返回值中的 t1 为“我很好”。
第三步:传递第2个变量 t2,api中提示:内容为“张无忌”,变量地址为36533296。OK,临时变量走一波。
![](https://aar.chengxu.online/upload/files/20230728/1690536163.png)
第四步:执行完毕,t2 的真实内容仍然为“张无忌”,返回值为“哈哈哈”,但返回值中的 t2 为“我很好”。
结论就是:跟上面一样,临时变量走一波耶~~~
API字符串传址方法示例 —— 先声明后调用:
以 User32.GetWindowTextA 为例,演示如何传递指针(文本指针、缓冲区指针、结构体指针)给api函数调用。
import console;
import win
var gwt,t
//定义传址的文本变量为 string 类型
gwt = ::User32.api("GetWindowTextA","int(addr,string,int)")
// 用string类型变量
t = string.repeat(255,'\x0')
gwt (..win.getForeground(),t,255)
console.log('定义为 string 类型,传递 string 类型,返回结果:\n'+t)
// 用buffer类型变量
t = raw.buffer(255)
gwt (..win.getForeground(),t,255)
console.log('定义为 string 类型,传递 buffer 类型,返回结果:\n'+ tostring(t))
//定义传址的文本变量为 pointer 类型
gwt = ::User32.api("GetWindowTextA","int(addr,pointer,int)")
// 用string类型变量
t = string.repeat(255,'\x0')
gwt (..win.getForeground(),t,255)
console.log('定义为 pointer 类型,传递 string 类型,返回结果:\n'+ t)
// 用buffer类型变量
t = raw.buffer(255)
gwt (..win.getForeground(),t,255)
console.log('定义为 pointer 类型,传递 buffer 类型,返回结果:\n'+ tostring(t))
//定义传址的文本变量为 struct& 类型
gwt = ::User32.api("GetWindowTextA","int(addr,struct&,int)")
// 用struct类型变量
t = {byte t[255]}
gwt (..win.getForeground(),t,255)
console.log('定义为 struct& 类型,传递 struct 类型,返回结果:\n'+ tostring(t.t))
console.pause(true);
执行结果:
![](https://aar.chengxu.online/upload/files/20230728/1690535429.png)
API字符串传址方法示例 —— 不声明直接调用:
t = string.repeat(255,'\x0')
User32.GetWindowTextA(..win.getForeground(),t,255)
console.log('传递 string 类型,直接调用:返回结果:\n'+ t)
t = raw.buffer(255)
User32.GetWindowTextA(..win.getForeground(),t,255)
console.log('传递 buffer 类型,直接调用:返回结果:\n'+ tostring(t))
t = {byte t[255]}
User32.GetWindowTextA(..win.getForeground(),t,255)
console.log('传递 struct 类型,直接调用:返回结果:\n'+ tostring(t.t))
执行结果:
![](https://aar.chengxu.online/upload/files/20230728/1690535547.png)
===============================================================
关于处理掉字符串尾部0的方法
aardio中字符串是允许包含字符0的,所以通过传址方式获取的字符串,在aardio中不会遇0终止,但是可以通过通过以下方法来截掉后面多余的 \0
t = string.repeat(255,'\x0')
gwt (..win.getForeground(),t,255)
//方法一(推荐)
t = ..raw.tostring(..raw.toPointer(t))
//方法二
t = ..string.trimright(t,'\0')
不声明直接传float数值的方法
因为上面提到过:
4、数值参数一律处理为32位int整型。(小于32位的整数、枚举类型、8位或32位bool值都跟int 32位数值兼容)
所以,如果要传递float数据类型,如何传递呢?
这里写了两个dll函数进行测试:
![](https://aar.chengxu.online/upload/files/20230728/1690534201.png)
编辑
aardio中调用方式总结如下(测试过程就不详细解释了,直接说结果吧):
var dll=..raw.loadDll()
//传值测试:
//数值型数据结构
dll.show_byvalue(123.456)
//错误(float传递时被自动处理为整数类型,值已发生改变)
dll.show_byvalue(..raw.float(123.456))
//正确(构造为小数类型,aardio自动识别并在调用时自动调整参数类型为float)
//传址型数据结构
dll.show_byvalue({float v=123.456})
//错误(传的是“地址”,不是“数值”,值不一样)
dll.show_byvalue(..raw.float(123.456,true))
//错误(传的是“地址”,不是“数值”,值不一样)
//数值型,数据类型转换,将float数值转为int数值,再进行传递
dll.show_byvalue(raw.convert({ float f = 123.456 },{int i}).i)
//正确(用int数值,伪造float类型的内存数据,能被dll正确解析为float数值。原理同union)
//总之,就是必须将正确的“值”传递过去。
//传址测试:
//数值型数据结构
dll.show_byaddr(123.456)
//错误,保护异常,退出
dll.show_byaddr(..raw.float(123.456))
//错误,保护异常,退出
//传址型数据结构
dll.show_byaddr({float v=123.456})
//正确
dll.show_byaddr(..raw.float(123.456,true))
//正确
//传址相对比较简单,其他方法不一一测试了。
===============================================================
结束语:
以上内容抛砖引玉,不保证完全正确,如果有不对之处,欢迎留言指正。