作者:庄晓立(Liigo)
日期:2015年3月3日夜
原创链接:http://blog.csdn.net/liigo/article/details/44045177
版权所有,转载请注明出处:http://blog.csdn.net/liigo
前两天我协助解决了一个技术问题,在此稍作记录和总结。
具体来说,就是在使用基于Webkit引擎的封装组件wke的过程中,需要把一个易语言函数注册给JavaScript引擎,让它可以在网页里被调用(就像在网页里调用普通JavaScript函数一样)。如果能做到这一点,就基本实现了从JavaScript传递参数到易语言、易语言返回值给JavaScript的双向沟通机制,以后有广泛的应用空间。
在整体思路上,还是蛮简单的。因为wke已经提供了颇为直观的接口函数(虽然严重缺乏文档):
#define JS_CALL __fastcall typedef jsValue (JS_CALL *jsNativeFunction) (jsExecState es); WKE_API void jsBindFunction(const char* name, jsNativeFunction fn, unsigned int argCount); WKE_API void jsBindGetter(const char* name, jsNativeFunction fn); /*get property*/ WKE_API void jsBindSetter(const char* name, jsNativeFunction fn); /*set property*/ WKE_API int jsArgCount(jsExecState es); WKE_API jsType jsArgType(jsExecState es, int argIdx); WKE_API jsValue jsArg(jsExecState es, int argIdx); ......
这里面最核心的函数是 jsBindFunction(),调用它就能注册一个新的JavaScript函数,只需提供函数名、实现回调函数、参数个数。在回调函数内部,通过 jsArgCount/jsArgType/jsArg 读取js传进来的参数,通过其他一些接口函数创建js值对象,都是一目了然的事情,这都不是事儿。
回调函数(fastcall)
首先卡在该回调函数的调用约定上:jsBindFunction的第二个参数,要求是 fastcall 调用约定的回调函数!可是易语言编译器根本就不支持编译生成fastcall调用约定的函数呀(仅支持stdcall)。fastcall 约定通过寄存器 ecx 和 edx 传递前两个参数,其余参数按照从右向左(从后往前)的顺序压栈,被调用者负责清理、平衡栈。这跟stdcall有一些类似但又明显不同。如果不管三七二十一盲目传递 stdcall
调用约定的回调函数进去,程序运行时非崩溃不可。
那怎么办呢?易语言编译器不支持fastcall,我们只好自食其力,纯手工生成二进制X86机器指令,人肉编译生成符合fastcall调用约定的回调函数。该函数声明的原型是:jsValue (__fastcall *jsNativeFunction) (jsExecState es),唯一个参数可从 ecx 寄存器中读取,没有入栈的参数,因而也不用平衡栈,直接 ret 就完事了。为了方便起见,我们引入两个易语言编写的函数:代理函数和用户函数,其中代理函数负责JS和易语言的类型转换,用户函数负责具体的执行逻辑,这两个函数毫无疑问都只能是stdcall调用约定(易语言编译器也不支持别的什么约定嘛)。下面设计我们的回调函数结构,以伪汇编代码来表示:
PUSH 用户函数地址 PUSH ecx MOV eax, 代理函数地址 CALL eax RET
这些伪汇编代码,要是用易语言写的话,其实就是一句话:返回(代理函数(es,用户函数))。(注:参数es是JavaScript引擎通过ecx寄存器传递进来的透明数据。)
易语言代码固然是简单,但因为编译器的限制,我们不能这么写。汇编代码稍微复杂一点,但我们仍然不能直接嵌入汇编(易语言编译器不支持)。只能手写机器码!把Intel指令集手册拿出来,查表,开工。既然是动态生成代码,当然需要先申请一块内存,然后把机器码填进去,然后把这块内存的首地址返回——这个内存的首地址也就是我们人肉编译生成的符合fastcall调用约定的回调函数的首地址。具体代码如下:
代理函数(stdcall)和用户函数(stdcall)
前面提到的代理函数,是一个很普通的易语言函数(stdcall),它负责解读JavaScript传递进来的参数,转换成易语言数据类型,转调易语言版的用户函数(也是stdcall),最后再把易语言用户函数的返回值转换为JavaScript类型后返回给JavaScript引擎。它接收两个参数,都是我们前面手工生成的回调函数传递进去的。代码如下:
代理函数的返回值是长整数型,也就是64位整数。根据 jsValue 的定义,它是64位指针,恰好可以用易语言的长整数表示。
JavaScript文本确定是UTF-8编码,转换到易语言文本之前,最好先执行编码转换(UTF-8 => GB18030),否则中文乱码。这一步骤非常简单,就作为课后作业吧。
我们完全可以改进这个代理函数,或者写另外一个代理函数,用于支持不同类型的用户函数(例如不同的参数类型和参数个数以及返回值类型)。
剩下的用户函数就更简单了,下面只是一个常规的示例(后面的测试代码就用到此函数):
把易语言函数注册为JavaScript函数
动态生成一个回调函数,作为参数传递给jsBindFunction即可:
函数调用次序总结
到了该总结一下的时候了:我们借助动态代码生成技术,在运行时生成一个符合fastcall调用约定的回调函数(jsNativeFunction),通过jsBindFunction将其注册到Javascript引擎,同时赋予其一个JavaScript函数名。网页脚本调用此JS函数时,回调函数被调用,进而回调函数又调用了代理函数,代理函数又调用了用户函数,用户函数返回后,返回值又被逐层返回给JS引擎。
测试代码
首先注册一个测试用的JS函数,易语言代码:注册JS函数("plus1",用户函数示例)。
再先来一段HTML,加载到浏览器中:
<a href='#' onclick="document.getElementById('result').value=plus1('liigo');">link</a> <p> <textarea rows='6' cols='36' id='result'>hello</textarea>
当点击网页中的链接时,之前注册的JS函数 plus1 将被执行,进而易语言函数 用户函数示例 被调用。易函数返回的文本,成了 plus1 的返回值,最终输出到网页内的编辑框中。如果编辑框中文本显示为“liigo hohoho”,说明测试成功。
写在最后的思考
能否将前面的实现方案用易语言内置函数 “置入代码” 替换?我(Liigo)想,至少有两个阻力妨碍我们在此应用置入代码:1、置入代码只能作用于已经存在的函数,而不能运行时动态生成新的函数;2、置入代码是编译时行为,置入的代码不能包含可变量(例如上文的 CALL eax 恐怕就行不通)。勉强应用置入代码,也不是不行,只是会让每一个用户函数都非常复杂,既包含了置入代码,又包含了JS和易语言的类型转换,还包含了业务逻辑,没有任何封装性可言,易用性约等于零。
全文完。谢谢收看!知道我是谁吗?
大名鼎鼎的御前四品带刀护士!