前言
前几天在aardio中使用我之前的htmlquery库写xpath提取正文时出现了一些bug,这篇文章来说下解决方法和更新下代码。
bug1
如果是正常单线程使用的话没有什么问题,如果是多线程使用的话就会出现意外崩溃的情况,也没有报错信息,进程直接就没了。
经过测试发现是raw.loadDll
导致的崩溃,测试代码如下(只在多线程里加载dll):
import console;
io.open()
thread.invoke(function(){
raw.loadDll(io.localpath("\htmlquery.dll"),,"cdecl");
})
while(true){
sleep(100)
}
console.pause(true);
然后就显示应用程序错误的弹窗:
一开始肯定是怀疑dll哪里没写对,看了半天dll的代码,发现我对golang不是很懂,代码还是完全防sunny的写法写的,也找不出哪里有问题。
然后我又尝试使用LoadLibraryW
来加载dll试试:
import console;
io.open()
thread.invoke(function(){
//raw.loadDll(io.localpath("\htmlquery.dll"),,"cdecl");
var path = ..io.localpath("\htmlquery.dll");
var LoadLibraryW = ::Kernel32.api("LoadLibraryW","addr(ustring libName)");
moduleAddr = LoadLibraryW(path);
})
while(true){
sleep(1000)
console.log("aaaa")
}
console.pause(true);
不过这样我怎么使用dll的导出函数呢?之前看process库代码时,我记得remoteApi这个函数就可以调用某个进程的某个dll的导出函数,当然也可以调用当前进程。再看下它的代码:
如果是传的地址就是调用的raw.module
,如果是传的符号则是调用raw.loadDll,我现在要绕过raw.loadDll,所以用raw.module
来测试下。
课外知识
aardio中怎么知道一个函数有哪些用法,除了去看文档和搜索源码,还可以根据输入提示大概看出怎么用:
例如上面显示有两种用法,第三个这是说明还有提示没有显示,可以选第三个按回车让它继续提示。
但是我是用raw.module("cdecl").api("ReleaseNode","void(int i)","cdecl");
它显示没有这个函数。用工具->探测器->DLL查看工具看dll是有这个导出函数的
那只能使用地址来调用了,怎么知道导出函数的地址呢?其实就是模块基址加偏移,基址就是LoadLibraryW的返回值。而导出函数的偏移是固定的,比如上面DLL查看工具的地址就是偏移。也可以通过下面的代码来获取:
import console;
import raw.pefile;
var dllPath = "\htmlquery.dll"
var pefile,err = raw.pefile(dllPath);
t = {}
e = pefile.getExportNames();
for(i=1;#e;1){
t[e[i].name] = e[i].address;
}
console.dumpJson(t);
console.pause(true);
然后就可以这样来调用:
exportFuncOffset = {
"ExistsAttr":1472864,
"FindOne":1473056,
"Find_Each":1472960,
"InnerText":1473152,
"LoadDoc":1473248,
"OutputHTML":1473328,
"Parse":1473440,
"Query":1473520,
"QueryAll_Each":1473616,
"ReleaseNode":1472800,
"SelectAttr":1473712,
"_cgo_dummy_export":3481092
};
ReleaseNode = ..raw.module("cdecl").api(moduleAddr+exportFuncOffset["ReleaseNode"],"void(int i)");
SelectAttrFunc = ..raw.module("cdecl").api(moduleAddr+exportFuncOffset["SelectAttr"],"bool(int i, string b,string a)");
QueryAll_Each = ..raw.module("cdecl").api(moduleAddr+exportFuncOffset["QueryAll_Each"],"bool(int nIndex,string name,POINTER callback)");
ExistsAttrFunc = ..raw.module("cdecl").api(moduleAddr+exportFuncOffset["ExistsAttr"],"bool(int nIndex,string name)");
Find_Each = ..raw.module("cdecl").api(moduleAddr+exportFuncOffset["Find_Each"],"bool(int nIndex,string name,POINTER callback)");
FindOneFunc = ..raw.module("cdecl").api(moduleAddr+exportFuncOffset["FindOne"],"int(int nIndex,string expr)");
QueryFunc = ..raw.module("cdecl").api(moduleAddr+exportFuncOffset["Query"],"int(int i, string a)");
ParseFunc = ..raw.module("cdecl").api(moduleAddr+exportFuncOffset["Parse"],"int(string a)");
OutputHTMLFunc = ..raw.module("cdecl").api(moduleAddr+exportFuncOffset["OutputHTML"],"bool(int i, bool b,string a)");
InnerTextFunc = ..raw.module("cdecl").api(moduleAddr+exportFuncOffset["InnerText"],"bool(int i, string a)");
LoadDocFunc = ..raw.module("cdecl").api(moduleAddr+exportFuncOffset["LoadDoc"],"int(string a)");
经过测试这样在多线程下就可以正常使用了。不过这样就无法使用aardio的内存加载dll了($
内嵌),每次运行都需要带着这个dll。
很多人喜欢追求单个exe文件,我其实很不理解这种做法。你看哪个程序不是分开好多dll和资源文件,全部打包到一个exe里是很不理智的做法。你可以打包到一起作为安装包,安装时解压释放到某个安装目录下。
bug2
还有一个问题就是内存释放不及时。ReleaseNode
调用时机比较晚,比如我们在release方法里加一行打印..console.log("Node release: ",owner.nIndex);
,然后循环调用解析xpath,你会发现打印的时机会非常晚,要到循环第六七次才开始释放。
所以需要重新设计一下代码,我想的是将整个弄成一个类。比如下面的代码,这样htmlQuery生成的对象销毁的时候就会释放所有go里面的内存。
class htmlQuery{
@_metaProperty;
}
namespace {
_metaProperty = ..util.metaProperty(
release = function(){
var nodeList = owner.nIndexList;
for(i=1;#nodeList;1){
//..console.log("release : ", nodeList[i]);
ReleaseNode(nodeList[i]);
}
owner.nIndexList = null;
};
)
}
使用上的话多加一句var htmlquery = htmlQuery()
,其他的用法都一样。
其他
其他几个库等我有时间再一个一个改,目前htmlquery已经改好并上传到了github: https://github.com/kanadeblisst00/aardio-extlibs
。
另外,dll源码也上传到了/dist/dll_code
里,有懂go的可以看下有没有哪里写错了。
目前extlibs.aardio
里的地址已经用不了了,你可以改成github的地址,或者直接下载仓库将lib
文件夹下的库代码拷贝到你自己的工程里。