Delphi也能解释执行吗?(中)

上一篇说到我们CnPack IDE专家包集成了RemObjects出品的开源Pascal Script引擎,能够在专家包中提供让用户动态执行Pascal脚本的功能。

我们是以源码形式集成编译的,其源码文件列表如下:

图片

单纯要在程序里跑起脚本来也不难,主要使用其uPSComponent.pas中提供的TPSScript类,给它的Script这个TStrings属性赋脚本内容的字符串列表值,再调用Compile方法进行编译,编译成功后,调用Execute方法进行执行,类似如下:

var  PSScript: TPSScript;begin  PSScript := TPSScript.Create(nil);  PSScript.Script.Text := 'program Test; begin end.';  if PSScript.Compile then    PSScript.Execute;  PSScript.Free;end;

但这只是万里长征开始了第一步,后面还有两万四千九百里来着。

图片

单纯创建TPSScript实例不难,但当我们准备真跑一些脚本的时候,就会发现,这脚本似乎什么都干不了。

为什么?因为几乎什么可用的“外部资源”都没提供。

Delphi的VCL框架是Delphi编译专用,并不能在Pascal Script脚本中直接使用。而且,哪怕脱离VCL框架的Pascal语言的标准库中的函数,比如Writeln和Readln,也同样有问题。

所以,丰富Pascal Script脚本可用性的原则,就是将“运行期一切可用的资源都注册到脚本引擎中”。

如何注册呢?这里就要了解脚本引擎提供的“编译期注册”,和“运行期注册”的两种途径了。

“编译期注册”的含义是,我告诉引擎,脚本需要调“声明是如此这般这般”的函数或类或方法或接口或常量变量。

“运行期注册”的含义则是,“编译期注册”的那些函数或类或方法或接口或常量变量等内容,真正跑起来用到的时候,我告诉引擎要去哪些地方寻找它们。

像不像头文件声明参与编译,及运行期动态链接?

图片

TPSScript类提供了两个事件,编译期注册事件OnCompImport和运行期注册事件OnExecImport,我们可以写这俩事件的处理函数:​​​​​​​

  PSScript.OnCompImport := PSScriptCompImport;  PSScript.OnExecImport := PSScriptExecImport;

这里拿标准库里的Readln和Writeln两个函数为例,我们的目的是要在脚本中使用这两个函数进行基本的输入输出,那么我们就得这样写:

编译期注册这两个函数的声明,让引擎知道该怎么调:​​​​​​​

procedure TCnScriptWrapper.PSScriptCompImport(Sender: TObject;  X: TIFPSPascalcompiler);begin  X.AddFunction('function Readln(const Msg: string): string;');  X.AddFunction('procedure Writeln(const Text: string);');end;

运行期则注册这俩函数的真正实现,搭起一个桥梁让它能跑起来:​​​​​​​

procedure TCnScriptExec.PSScriptExecImport(Sender: TObject; Exec: TIFPSExec;  X: TIFPSRuntimeClassImporter);begin  Exec.RegisterFunctionName('Readln', _Readln, Self, nil);  Exec.RegisterFunctionName('Writeln', _Writeln, Self, nil);end;

桥梁里引用到的两个函数_Readln和_Writeln需要我们额外实现,我们在_Readln中使用InputQuery代替其输入功能,在_Writeln中用一个Memo接收输出的字符串,这样就形成了一个让脚本跑起来时能接收输入并显示输出的功能闭环。

图片

按互联网的黑话来说,“闭环”完成了,下面该打“组合拳”了。

如果每个脚本要用到的函数都要像Readln和Writeln那样写一大堆实现,势必相当麻烦,为了省事,Pascal Script引擎提供了Plugin的机制,以及自动生成注册Plugin的功能。

它的机制用一句话就可以概括:我们可以继承uPSComponent.pas中的TPSPlugin类,在子类的CompileImport1方法中写好编译期注册内容,在ExecImport1方法中写好运行期注册内容,再把这子类创建个实例,给Add到TPSScript实例的Plugins属性中。这样脚本引擎实例跑起来的时候,就会遍历加到自身的Plugins实例,顺序调用其俩注册方法,把我们需要注册给引擎的东西统统塞进去。

而且更为方便的是,Pascal Script引擎提供一Unit Importer功能,能够根据我们需要注册的单元内容,自动生成一个对应的Plugin单元,里面有自动生成的Plugin实现,供我们直接使用。

这考虑到VCL框架,就稍微……有点省事了。

图片

思维灵活的朋友们一定想到了,我们集成脚本引擎的Delphi的EXE程序本身就使用到了完整的VCL框架,我们是否可以把VCL框架涉及到的单元“统统”注册到脚本引擎中,让脚本能完全自由使用呢?

答案当然是(绝大部分)肯定的!

(不把话说死的原因是,引擎有语法限制,确实有些东西注册不进去)

这些微小的工作,CnPack IDE专家包中已经做得差不多了,我们源码的cnwizards\Source\ScriptWizard目录下有大量CnScript_*.pas文件,均是针对Delphi的VCL框架、Delphi的ToolsAPI接口、以及专家包本身的工具类等,所编写生成的脚本注册Plugin文件。

举例说明,从System单元中声明的基础类TObject开始,我们在CnScript_System.pas单元里是这么写的:​​​​​​​

procedure TPSImport_System.CompileImport1(CompExec: TPSScript);begin  SIRegister_System(CompExec.Comp);end;

SIRegister_System的实现:​​​​​​​

procedure SIRegister_System(CL: TPSPascalCompiler);begin  ……  CL.AddTypeS('THandle', 'LongWord');  ……  SIRegister_TObject(CL);  ……end;

SIRegister_TObject的实现(注意RegisterMethod里没有class的字样,表示仅是普通function):​​​​​​​

procedure SIRegister_TObject(CL: TPSPascalCompiler);begin  with CL.AddClass(nil, TObject) do  begin    RegisterMethod('Constructor Create');    RegisterMethod('Procedure Free');    RegisterMethod('Function ClassName : string');    RegisterMethod('Function ClassNameIs( const Name : string) : Boolean');    RegisterMethod('Function ClassInfo : Pointer');    RegisterMethod('Function InstanceSize : Longint');    RegisterMethod('Function MethodAddress( const Name : string) : Pointer');    RegisterMethod('Function MethodName( Address : Pointer) : string');    RegisterMethod('Function FieldAddress( const Name : string) : Pointer');  end;end;

总而言之,这里把TObject类及其各个方法,包括构造函数、析构函数等,全都在编译期注册到脚本引擎中了。

然后,运行期注册是这么写的:​​​​​​​

procedure TPSImport_System.ExecImport1(CompExec: TPSScript; const ri: TPSRuntimeClassImporter);begin  RIRegister_System(ri);end;

RIRegister_System的实现是:​​​​​​​

procedure RIRegister_System(CL: TPSRuntimeClassImporter);begin  RIRegister_TObject(CL);end;

RIRegister_TObject的实现则是:​​​​​​​

procedure RIRegister_TObject(CL: TPSRuntimeClassImporter);begin  with CL.Add(TObject) do  begin    RegisterConstructor(@TObject.Create, 'Create');    RegisterMethod(@TObject.Free, 'Free');    ……  end;end;

可以看出,运行期注册的函数,将设计期注册的名为Create的方法,动态关联到了@TObject.Create上,Free方法也关联到了@TObject.Free,这样就在脚本引擎中给脚本及TObject类的方法都搭起了桥,我们就能在脚本中动态创建TObject及Free它了:​​​​​​​

program TestObj;var  Obj: TObject;begin  Obj := TObject.Create;  Writeln(Obj.ClassName);  Obj.Free;end;

System单元中的TObject是较为简单的,我们还有SysUtils、Classes、Controls、Forms,甚至各个版本Delphi对应的ToolsAPI等单元的注册,几乎把大半个VCL框架都搬进来了。它们的第一个版本是使用UnitImporter生成的,后面则跟随Delphi版本升级而维护改动,持续为CnPack IDE专家包用户提供动态跑脚本操纵Delphi IDE的功能。


细心的读者可能已经注意到,TObject在System.pas中的方法声明,绝大多数都是class function,可以脱离实例而调用,如:​​​​​​​

    class function ClassName: ShortString;    class function ClassNameIs(const Name: string): Boolean;    class function ClassParent: TClass;    class function ClassInfo: Pointer;    class function InstanceSize: Longint;

但我们在编译期注册脚本时,写的内容却省掉了class,这样会不会出问题?

答案是有问题,但被特定的小把戏绕过去了。

原来,Pascal Script所实现的Object Pascal语法,离Delphi的还有差距,class function和class procedure便属于不支持之列。如果脚本要调用class function或class procedure,Unit Importer在根据源码文件生成注册的Plugin源码的时候,是使用了一点小把戏从而达到目的了的。

究竟是什么小把戏,且听下回分解。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值