aardio - API调用分析

光庆 2023-7-28 2231

转自:卢光庆

于 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代码:

编辑

aardio调用代码:

没有赋予struct初始值,int类型的值默认为0

传递给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代码:

编辑

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);

执行结果

编辑

编辑

执行结果:传递进去数值正常;修改结构体数据失败;返回值正常;结构体未作为返回值返回;

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);
执行结果:

编辑

编辑

执行结果:传入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代码:

编辑

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、执行结果:

执行结果:传入api文本正常;api中修改文本变量数据失败;返回值正常;传出参数修改后的数据作为返回值返回。

4、aardio调用示例(不传址):

执行结果:传入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、返回一个文本值

1、用两个内容【一样】的string变量进行【不传址】测试。

第一步:传递第1个变量 t1,api中提示:内容为“张三丰”,变量地址为 36657216

第二步:api中修改了变量 t1 的内容为“我很好”,aardio中console也同步显示,返回值为“哈哈哈”,t1为“我很好”,OK,正确无误。

第三步:传递第2个变量 t2,api中提示:内容为“我很好”,变量地址为 36657216。

那么问题来了,为什么 t2 不是 “张三丰” 呢?

那是因为aardio中,同样内容的文本变量,共享同一内存地址。

你改了一个,等于是改了所有。

这也是为什么这两个变量传递进去的“变量地址”是一样的原因。

所以,这里传给api的,就是string变量的“真实地址”。

第四步:执行完毕,毫无悬念。

结论就是:t1 、t2 内容一样,地址一样,api改一个变量的内存数据,等于全改了。“不传址”的情况下,传给api的反而是真实地址。

2、用两个内容【不一样】的string变量进行【不传址】测试。

第一步:传递第1个变量 t1,api中提示:内容为“张三丰”,变量地址为 27875392

第二步:api中修改了变量 t1 的内容为“我很好”,aardio中console也同步显示,返回值为“哈哈哈”,t1为“我很好”,OK,正确无误。

第三步:传递第2个变量 t2,api中提示:内容为“张无忌”,变量地址为 27875424。

第四步:执行完毕,变量 t2 内容变为“我很好”。

执行完毕,结论就是:t1 、t2 内容不一样,地址也不一样,“不传址”的情况下,传给api的都是真实地址,都被api成功修改。

3、用两个内容【一样】的string变量进行【传址】测试。

因为string传址会被aardio作为返回值返回,也就是说api会有多个返回值,所以我们下面的测试代码中,将api的执行结果放在console.log的最后一个参数,以便于能全部显示出来。

第一步:传递第1个变量 t1,api中提示:内容为“张三丰”,变量地址为27423792。

第二步:api中没有成功修改变量 t1 的内容为“我很好”,t1 的真实内容仍然为“张三丰”。返回值为“哈哈哈”,但返回值中的 t1 为“我很好”。

第三步:传递第2个变量 t2,api中提示:内容为“张三丰”,变量地址为 27423792。

同样,t1、t2 内容一样,地址一样,因为 t1 没有被修改,所以 t2 传进去,还是 “张三丰”。

所以,这里传给api的,应该不是string变量 t1 的“真实地址”。

但 api 却成功修改并返回了修改后的“正确”结果,所以猜想这里用了一个临时变量去“欺骗”api,而非真实的 t1。

而且,两次传递的临时变量地址是一样的,但第一次传递后,明明内容被修改了,第二次传递却又恢复了。

这说明:aardio将临时变量传递给api时,是先给临时变量同步了真实变量的值。

第四步:执行完毕,结论就是:“传址”的情况下,先把真实变量的值,复制给临时变量,再把临时变量传给api。api改的是临时变量的值,返回的也是临时变量的值。

4、用两个内容【不一样】的string变量进行【传址】测试。

第一步:传递第1个变量 t1,api中提示:内容为“张三丰”,变量地址为36533296。

第二步:api中没有成功修改变量 t1 的内容为“我很好”,t1 的真实内容仍然为“张三丰”。返回值为“哈哈哈”,但返回值中的 t1 为“我很好”。

第三步:传递第2个变量 t2,api中提示:内容为“张无忌”,变量地址为36533296。OK,临时变量走一波。

第四步:执行完毕,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);

执行结果:

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))

执行结果:


===============================================================

关于处理掉字符串尾部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函数进行测试:

编辑

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))
        //正确
 
    //传址相对比较简单,其他方法不一一测试了。

===============================================================

结束语:

以上内容抛砖引玉,不保证完全正确,如果有不对之处,欢迎留言指正。

最新回复 (6)
  • 阿甘 2023-7-30
    0 2
    收藏了,感谢分享!
  • Viewer8122 2023-7-30
    0 3

    优秀~!

  • coso 2023-8-4
    0 4
    厉害
  • 光庆 2023-8-4
    0 5
    此楼层已删除
  • xianzhi0520 2023-11-30
    0 6
    大佬 厉害
  • ccbwx 2023-11-30
    0 7
    大佬 厉害,收藏了
返回