本节介绍如何将 aardio 动态函数转换为原生回调函数指针。
stdcall 调用约定由被调用函数恢复堆栈。cdecl 调用约定由主调函数恢复堆栈。
一般系统 API 使用 stdcall 调用约定,纯 C 语言实现的 DLL 通常使用 cdecl 调用约定。
在一个 DLL 中通常会统一使用一种调用约定,如果 API 为 stdcall 调用约定,则回调函数一般也是 stdcall 调用约定。
参考:API 调用约定
WinAPI 回调函数使用 stdcall 约定,stdcall 是调用外部函数时使用最普遍的调用约定。使用 raw.tostdcall函 数将普通 aardio 函数转换为支持 stdcall 调用约定的可回调函数。
示例:
var func = function(a,b,c) {
io.print(owner,a,b,c)
}
var func_c = raw.tostdcall( func,"(int,int,int)" );
//回调函数也可以在 aardio 中被调用执行,例如:
func_c(1,2,3)
raw.tostdcall 的第一个参数为函数对象。 第二个参数为函数原型,在原型中指明参数与返回值的 原生数据类型,其语法与 声明 API 相同。
原生回调函数由 aardio 内存管理器自动进行垃圾回收。 如果回调函数仍然被外部函数调用,您必须保持原生回调函数在有效作用域内。
回调函数是一个 cdata 对象,回调函数声明了 _topointer 元方法,因此可以自动转换为pointer 指针。
可以从一个 aardio 函数创建多个回调函数。
为区别不同的回调函数,可以附加一个参数以显式指定 owner 指针的值。
示例:
var func2_c =raw.tostdcall( func,"(int,int,int)" ,"可以附加一个参数,以指定被回调函数的 owner 值" );
使用 raw.tocdecl 函数将普通 aardio 函数转换为支持 cdecl 调用约定的可回调函数。
除了将 raw.tostdcall 函数改为 raw.tocdecl ,其他用法与上面的 raw.tostdcall 一样。
使用 raw.tofastcall 函数将普通 aardio 函数转换为支持 fastcall 调用约定的可回调函数。
除了将 raw.tostdcall 函数改为 raw.tofastcall ,其他用法与上面的 raw.tostdcall 一样。
普通回调函数创建于当前线程环境,
不能将普通回调函数指针传递给其他线程使用。
如果一个原生回调函数不是在当前线程被触发,应作如下替换:
上述 thread 名字空间下的函数与 raw 名字空间下的同名函数参数与用法相同,
区别是 thread 名字空间提供的函数创建的原生回调函数是可以多线程使用的。
要注意在多线程回调函数里要遵守 aardio 的 多线程开发规则。 例如要注意 import 等语句要写到函数内部, 这与创建线程的规则相同,因为不同线程的变量环境各自独立。
当使用 thread.tostdcall, thread.tocdecl() thread.tofastcall() 等函数创建多线程回调函数时,aardio 将会自动初始化线程环境 - 并且可以共享、重用已创建的线程环境。
即使线程不是由 aardio 代码所创建,aardio 仍然会自动管理所有被动创建的线程环境,在回调函数对象 离开作用域,或者发起调用的线程结束时 aardio 根据需要自动回收资源。
如果所有引用原生回调函数对象的变量被赋值为 null 或者超出作用域不再有效,
将会由 aardio 内存管理器自动回收并删除。
因此:如果原生回调函数对象仍然被外部API函数调用,你必须确定原生回调函数在有效作用域内,并且没有被赋值为null。 这一点非常重要,你释放了原生回调函数,可是外部组件的C、C++却并不知道,如果原生回调函数对象已被回收,再次调用原生回调函数将会导致不可预知的错误。
如果原生回调函数被指定为某对象的成员,在函数内部又循环引用了该对象,
这会导致无法释放资源。使用参数 @3 将该对象指定为 owner 可避免该问题
下面是一个错误用法的示例:
this.createCallback = function(callback){
this.callback = ..raw.tostdcall(
function(...){
this.beforeCallback(...) //这里导致了循环引用自身的问题
return callback(...);
},proto);
}
正确用法是使用 owner 参数引用对象自身,如下:
this.createCallback = function(callback){
this.callback = ..raw.tostdcall(
function(...){
owner.beforeCallback(...)
return callback(...);
},proto,this);
}
注意只有线程内原生回调函数要注意这个问题,多线程原生回调函数无法跨线程直接引用对象,而普通函数不会有这些问题。
无论是 COM 事件回调还是原生 API 回调函数,它们都有一个共同特征是要避免上述的循环引用。而纯 aardio 对象或函数不存在这种问题。参考:避免 COM 事件表的循环引用
原生回调函数的返回值类型检测较宽松,支持按 C 语言规则兼容转换。
null 值返回 0,true 返回 1, false 返回 0,所有数值、浮点数值,布尔值、null值,math.size64 值等声明类型与实际返回值类型可自动转换。返回其他类型则尝试转换为指针值返回。
但原生回调函数返回值声明为 struct 类型时则必须返回一个不大于 64 位的结构体,且传值而非传址(与 struct 用在参数中总是传址不同)。
原生回调函数声明中不指定返回值或指定为 void 无返回值 (但仍然可以通过输出参数添加不定个数的返回值)。
在原生回调函数声明中,可以在参数类型后添加&
声明输出参数,例如C,C++中的int *
指针可以声明为int &
, 在 aardio 函数中使用返回值输出参数(有几个输出参数,增加几个返回值),例如:
cdeclCallback = raw.tocdecl( function(a,b){
//使用返回值输出参数(有几个输出参数,增加几个返回值)
return 0,a,456;
},"int(long &a,int &b )")
也可以把参数声明为指针,再自己修改指针的值,示例:
cdeclCallback = raw.tocdecl( function(a,b){
raw.convert({long v = 123},a);
return 0;
},"int(ptr a,int b )")
原生回调函数中,对于 struct 型参数,可以用一个实际的结构体声明替换 struct 类型,例如 raw.tocdecl(func,"int(struct pt)")
我们可以更改为 raw.tocdecl(func,"int({int x;int y} pt)")
,也可以在结构体后面加一个&
表示输出类型的参数,并可通过返回值修改调用者传入的原结构体的值(回调函数无对应返回值时忽略此操作),
在回调函数中直接声明结构体时,要注意结构体不能再包含其他结构体(简单说只能出现一对{}
),并且除了结构体的字段名以外 - 不能在该结构体声明中使用其他变量名。
枚举与遍历桌面软件可以直接使用标准库函数 winex.enum, winex.each 更方便一些。
WinAPI 回调函数使用 stdcall 约定,stdcall 是调用外部函数时使用最普遍的调用约定。
调用 ::User32.EnumWindows 枚举桌面窗口示例:
import console;
//回调函数
var enumwndProc = function(hwnd,lparam ){
//在控制台显示所有参数
console.log("回调owner:" + owner.tag , "窗口句柄:" + hwnd , "回调附加参数:" + lparam );
return 1 ; //如果有输出的传址参数,在这里按传入顺序添加所有参数到返回值后面
}
//转换为原生回调函数
enumwndProc_c = raw.tostdcall(enumwndProc,"int(int hwnd,int lparam )" ,{tag='crane'} );
//回调函数在 API 参数中可自动转换为 pointer 类型指针。
::User32.EnumWindows(enumwndProc_c ,12330);
//可以确定回调函数不再使用,赋值为null,并等待由内存垃圾回收
enumwndProc_c = null;