Site Map Contact Us Home
E-mail Newsletter
Subscribe to get informed about
Clever Components news.

Your Name:
Your Email:
 
SUBSCRIBE
 
Previous Newsletters
 




Products Articles Downloads Order Support
Customer Portal     

My experience using RemObjects PascalScript to run scripts in Delphi

Happy Halloween! Buy Clever Comonents products at -20% discount!

Don't miss the chance to buy Internet and Database components at the most favorable price!

Only 13 days, until 9th of November.

20% off! BUY NOW!

 


Run Pascal Script in Delphi

Introduction

Some time ago, I had a task to update a legacy app written in Delphi 7, convert, and compile it using the latest version of RAD Studio. This was before 10.4 release, and our choice was Delphi 10.3 Rio Community Edition.

The most of source code was migrated without any significant problems, except for one, which runs scripts in Delphi.

The program used DCScript by DreamCom to run VBScript, JavaScript, and Pascal scripts. DCScript is not supported anymore, and DreamCom does not exist. Therefore, I started researching a good replacement. After googling and reading clever articles, my choice was PascalScript by RemObjects Software.

So, to make things working, I had to solve the following tasks:

  • Install Pascal Script;
  • Write a simple app to understand how this scripting engine works;
  • Pass own variables and types to a script;
  • Run scripts in threads.

 

Install the necessary components

The sources for PascalScript are free and can be downloaded on GitHub

First, we need to compile and install the PascalScript Core library. For my Delphi 10.3, I used PascalScript_Core_D26.dproj. The process produced numerous compilation warnings. Most of them were concerned with non-Unicode functions. This is annoying, but in fact, PascalScript was initially developed before Delphi 2009 and Unicode support. There was a great desire to fix them all. But, let us go ahead and run our first script.

 

Run a test script

The most common task for a script in our program is to open, modify, and write text files. Therefore, I decided to test a new script engine using the following script:

 

// [Pascal Script]
var
   list: TStringList;
begin
   list := TStringList.Create();
   try
      list.Add(‘Hello World’);
      list.SaveToFile(‘c:Users\Public\Test.txt’);
   finally
      list.Free();
   end;
end.

 

Here, I should make a small clarification: DCScript supported many scripting languages, including VBScript, JavaScript, and PascalScript. Most of our scripts are developed in VBScript. However, due to difficulties with choosing an acceptable engine, VBScript will not be supported in the nearest future. Therefore, most likely, we will have to convert all our scripts to PascalScript or JavaScript. I will write about JavaScript implementation in the next article.

To be simple, let us put a button and the following two components onto a form: TPSScript and TPSImport_Classes. The first one compiles and runs the script, and the last one imports VCL objects and structures from the Classes module into the script.

PascalScript consists of two independent parts: uPSCompiler.pas and uPSRuntime.pas. The TPSScript component combines both these parts, compiles, and runs the script. You can do that using the Compile and the Execute methods, accordingly.

The script compiler provides detailed information about errors, including the line number, and the position in the line of the script that caused the problem. You can find it within the CompilerMessages array property. Let us collect all error messages as a simple string.

 

// [Delphi]
function TForm1.GetCompilerMessageStr(AScript: TPSScript): string;
var
   i: Integer;
begin
   Result := '';
   for i := 0 to AScript.CompilerMessageCount - 1 do
   begin
      Result := Result + string(AScript.CompilerMessages[i].MessageToString()) + '#13#10';
   end;
end;

 

Wow, the script runs, and even creates a file on the disk.

 

Import own class into the script

The original application actively exchanges data with scripts being running. In addition, it uses its own Delphi classes that must be available within the script. Let us implement a simple class, TJobParam, and import this class to the PascalScript engine.

 

// [Delphi]
unit JobParams;

interface

type
   TJobParam = class
   private
      FValue: string;
   public
      procedure AddText(const AText: string);
      property Value: string read FValue write FValue;   
   end;

implementation

{ TJobParam }

procedure TJobParam.AddText(const AText: string);   
begin
   Value := Value + AText;
end;

end.

 

Good guys from RemObjects have made a special utility for importing classes, which will be used in a script. This utility is also available on GitHub However, attempts to compile revealed several problems. This program requires a third-party SynEdit library, which is not supported by the latest version of Delphi. I found packages for XE5, and lower. Therefore, I decided to compile the program on my old Delphi 2007 I have.

 

 

Finally, we got a Delphi unit, uPSI_JobParams.pas, which provides a special plugin class with a proxy code for our TJobParam, similar to TPSImport_Classes.

Now, we can create an instance of the TJobParam class and save it to a private field, which I declared within the MainForm class. You need to handle both the TPSScript.OnCompile and the TPSScript.OnExecute events to register this field in the script engine. The OnCompile event handler calls AddRegisteredPTRVariable for registering the used type. The other handler, OnExecute, passes a pointer to this field into the script. Please check out the following article to learn more about using classes with PascalScript

 

// [Delphi]
procedure TForm1.ScriptCompile(Sender: TPSScript);
begin
   Sender.AddRegisteredPTRVariable('JobParam', 'TJobParam');
end;

procedure TForm1.ScriptExecute(Sender: TPSScript);
begin
   Sender.SetPointerToData('JobParam', @FJobParam, Sender.FindNamedType('TJobParam'));   
end;

 

There are two ways of using the plugin class from uPSI_JobParams.pas. You can install TPSImport_JobParams as a component, and put it onto the form, in the same way as TPSImport_Classes. The other way is to create plugins at runtime. The last one is preferable because the original program runs scripts in threads. No forms are used. So, I created the imported plugin at runtime, removed the TPSImport_Classes instance from MainForm, and created it at runtime, as well.

 

// [Delphi]
constructor TForm1.Create(AOwner: TComponent);
var
   jobParamsPlugin: TPSImport_JobParams;
   classesPlugin: TPSImport_Classes;
   script: TPSScript;
   pluginItem: TPSPluginItem;
begin
   inherited Create(AOwner);
   script := TPSScript.Create(nil);
   script.OnCompile := ScriptCompile;
   script.OnExecute := ScriptExecute;

   classesPlugin := TPSImport_Classes.Create(nil);
   pluginItem := script.Plugins.Add() as TPSPluginItem;
   pluginItem.Plugin := classesPlugin;

   jobParamsPlugin := TPSImport_JobParams.Create(nil);   
   pluginItem := script.Plugins.Add() as TPSPluginItem;
   pluginItem.Plugin := jobParamsPlugin;

   FJobParam := TJobParam.Create();
   FJobParam.Value := 'Test';
end;

destructor TForm1.Destroy;
begin
   FJobParam.Free();
   classesPlugin.Free();
   jobParamsPlugin.Free();
   script.Free();
   inherited Destroy();
end;

 

Run scripts in threads

The fast-and-easy way to check if the script engine works in threads is to run it in an anonymous thread. Now, Delphi provides a nice function - TThread.CreateAnonymousThread.

 

// [Delphi]
thread := TThread.CreateAnonymousThread(
   procedure
   var
      jobParamsPlugin: TPSImport_JobParams;
      classesPlugin: TPSImport_Classes;
      script: TPSScript;
      pluginItem: TPSPluginItem;
   begin
      script := nil;
      classesPlugin := nil;
      jobParamsPlugin := nil;
      try
         script := TPSScript.Create(nil);
         script.OnCompile := ScriptCompile;
         script.OnExecute := ScriptExecute;

         classesPlugin := TPSImport_Classes.Create(nil);
         pluginItem := script.Plugins.Add() as TPSPluginItem;
         pluginItem.Plugin := classesPlugin;

         jobParamsPlugin := TPSImport_JobParams.Create(nil);
         pluginItem := script.Plugins.Add() as TPSPluginItem;
         pluginItem.Plugin := jobParamsPlugin;

         script.Script.Clear();
         script.Script.Add('... the script follows here

         if not script.Compile then
            raise Exception.Create('Script compilation errors: ' + GetCompilerMessageStr(script));   

         if not script.Execute then
            raise Exception.Create('Script execution errors');
      finally
         jobParamsPlugin.Free();
         classesPlugin.Free();
         script.Free();
      end;
   end);

 

However, the original program utilizes a TThread descendant to run scripts. Therefore, I changed my code as follows:

 

// [Delphi]
TScriptThread = class(TThread)
private
   FJobParam: TJobParam;
   function GetCompilerMessageStr(AScript: TPSScript): string;   
   procedure ScriptCompile(Sender: TPSScript);
   procedure ScriptExecute(Sender: TPSScript);
protected
   procedure Execute; override;
public
   constructor Create(AJobParam: TJobParam);
end;

 

Here, both the ScriptCompile and the ScriptExecute methods are event handlers for the TPSScript.OnCompile and the TPSScript.OnExecute events, accordingly. The Execute method contains code, which runs the script (see the previous code listing).

 

Pass parameters from Delphi to the script, and back

To exchange data with a script, which runs in a thread, I created an instance of TJobParam and passed it to the TScriptThread.Constructor. The mentioned event handlers (ScriptCompile and ScriptExecute) do all the work.

 

// [Delphi]
thread := TScriptThread.Create(jobParam);
try
   thread.FreeOnTerminate := False;
   thread.Start();
   thread.WaitFor();
finally
   thread.Free();
end;

ShowMessage('Result: ' + jobParam.Value);

...

procedure TScriptThread.ScriptCompile(Sender: TPSScript);
begin
   Sender.AddRegisteredPTRVariable('JobParam', 'TJobParam');
end;

procedure TScriptThread.ScriptExecute(Sender: TPSScript);
begin
   Sender.SetPointerToData('JobParam', @FJobParam, Sender.FindNamedType('TJobParam'));   
end;

 

Conclusion

The implemented test program showed good results. You can download the Sources on GitHub

The selected script engine can perform all tasks we need and can work in threads. We have already incorporated this solution into our original app - TaskRunner. Recently, we have open-sourced it - TaskRunner on GitHub The program still has some legacy user interface, but we are working on this task.

TaskRunner serves to automate the Software build tasks, deploying builds, and can also be used for many other tasks. You are free to clone or fork this repository. Your opinion, suggestions, and improvements will be greatly appreciated.

We are on FaceBook

Twitter

Telegram

 

Nikita Shirokov
Clever Components team
www.clevercomponents.com

    Copyright © 2000-2020