为Delphi应用增加脚本支持
上次说到我想为DirectUI增加脚本支持,今天我们就来研究下脚本的实现问题。
虽然现在有了诸如AntLR、GOLD Parser、TP Lex and Yacc等等CC工具,大大方便了脚本引擎的开发,但我仍然觉得在这个框架里自己实现一套脚本引擎是件毫无意义的事。所以我决定使用现有的脚本引擎。
事实上Delphi可用的第三方脚本引擎很多,这里列几个常见的:
FastScript
号称最快的Delphi脚本引擎,支持 VB、JS、Cpp、Pascal 语法,可以在脚本中使用自定义的类型和对象,内置了对Delphi VCL的支持。
ifps/RemObjects PascalScript
使用Pascal语法的脚本引擎,可以在脚本中使用自定义的类型和对象。
TMS Scripter Studio
TMS出品的脚本引擎,除了脚本引擎,还附带有一堆可视控件,可以用来开发IDE。支持在脚本中使用自定义的类型和对象,也内置了对Delphi VCL的支持。
PaxScript / PaxCompiler
这是我用过的Delphi里最强大的脚本引擎了。同样,她支持 VB、JS、Cpp、Pascal 语法,也支持在脚本中使用自定义的类型和对象,内置了对Delphi VCL的支持。另外,她还支持将脚本代码编译成中间字节码,以后运行是可以直接载入中间字节码执行,节约了代码解析的时间。如果使用PaxCompiler,你甚至可以将脚本代码编译成机器码直接执行!
但是最终我并没有在其中进行选择,因为上述这些脚本引擎都有一个问题,就是对自定义的类型和对象的支持方式。
在这些引擎中,如果你要增加自定义类型的支持,就必须为每一个类的每一个方法和属性生成一个代理函数,然后通过引擎通过这些代理函数对对象进行操作。
想想我的DirectUI引擎,其中大大小小有上百个类,光生成这些代理函数都能让我疯了。虽然它们也有提供工具,可以根据源代码直接生成代理函数,但问题是我以后如果修改了源代码,岂不是又得重新生成一遍?想想都烦啊。
最后我把目光定在了MS身上。其实MS早就为我们提供了一套好用的脚本引擎:MS ScriptControl。见过HTML里的VBS和JS吗?那里用的脚本引擎跟MS ScriptControl是同一套核心。
MS ScriptControl系统默认支持VBS和JS,但实际上他的语法是可以扩展的。例如你机器上如果装有Python,你就能在脚本中使用Python。
MS ScriptControl可以访问自动化对象(对应到Delphi里就是TAutoObject),将对象用AddObject方法注册到引擎里,就可以在脚本中访问了。在运行时,引擎会取得对象的IDispatch接口实现,并通过IDispatch.GetIDsOfNames取得要访问的方法或属性的ID,然后在调用IDispatch.Invoke执行此方法或取得此属性的值。
聪明的你一定想到了,我只要实现一个TAutoObject代理对象,然后用这个对象通过RTTI,就可以访问到我们的自定义对象了!借助RTTI,我只需要实现一个通用的代理对象,就能让脚本访问所有类型的对象,即使类型有更改,代理对象都不需要做任何改动!真正的一劳永逸啊,哇咔咔~~~
好了,饭要一口一口吃,现在我们先把MS ScriptControl封装一下,后面再去做代理对象。
我们先来定义一套接口,这套接口定义了我们的脚本引擎所需要实现的功能,然后再封装MS ScriptControl来实现。这样做的好处是,如果我以后想换其他的脚本引擎,只需要把这套接口重新实现一遍就好了,外部调用脚本引擎的地方基本无需改动。
代码不贴上来了,后面有下载。
这里还要说说MS ScriptControl的一些东西。其实网上相关的文章不少,不过大多都是泛泛而谈,有用的东西不多,我只好自己研究了。
简单的东西就不写了,去google吧,这里讲几个网上找不到相关说明的细节。
MS ScriptControl中有模块的概念,就像我们IDE中同一工程下不同的单元文件一样。默认有一个主模块,名字叫Global,MS ScriptControl中的AddCode、Eval、ExecuteStatetment等方法都是对这个模块访问的封装。其他模块是需要用Modules.Add自己添加的。
这里有个细节,Modules.Add方法有2个参数:Name,模块名称,必填;Obj,OleVariant类型,是个可选参数。这个参数做什么用的我到现在也没明白,本来我也不想关心他的,可惜绕不过去。在VB中,DispInterface的可选参数可以不写,直接Modules.Add("Module1")就OK,可是Delphi下这个参数是var的,必须填。于是我
Obj: OleVariant;
begin
Obj := Unassigned;
Modules.Add('Module1', Obj);
end;
运行报错。。。于是我再Obj := Null,错。。。再Obj := 0,又错。。。数次失败后,我都快绝望了。。。后来我想,这个参数我也不用,不是有个全局的EmptyParam么,省了我声明个变量,于是Modules.Add('Module1', EmptyParam),结果。。。通过了。。。神那,你能告诉我这参数到底干虾米用的啊。。。泪啊。。。
继续。。。与编译器对待单元文件的方式不同,这些模块都是封闭的,除了其他的模块可以直接调用主模块中方法外,其他模块之间无法直接互相调用,主模块也无法调用其他模块。挺奇怪的。
后来我想了个辙。每个模块都有个属性CodeObject,即该模块的代码对象,通过他是可以调用到模块中的代码的。这个对象可是IDispatch哦,那么我将他注册到脚本引擎里,不就可以在其他模块中用 模块名.对象 来访问了吗?我真是太聪明了,咔咔。。。于是乎,就有了下面的代码
I: Integer;
Module: IScriptModule;
begin
for I := 1 to Modules.Count do
begin
Module := Modules[I];
FScript.AddObject(Module.Name, Module, False);
end;
end;
F5,嗯,出错?对象名已存在??NN个熊,要存在我怎么访问不到!算了,只好出昏招,我注册名跟模块名不一样,总可以了吧。。。
唉,折腾了一天,总算是可以用了~
图上是调用第一个模块的Foo方法的执行结果
好了,罗罗嗦嗦了这么多,就到这里吧,下次再来实现代理对象。