Skip to content
November 28, 2009 / lawrencebarsanti

Introduction to Pascal Script

I recently started using Pascal Script and the only useful documentation I could find were these (one, two) tutorials. These two tutorials utilize the TPSScript component and Unit Import tool which, in my opinion, make Pascal Script cumbersome to use.   Because of this, I decided to write this article which should help you use Pascal Script effectively.

I will use this example to teach you how to:

  1. Write Pascal Scripts
  2. Compile and Execute Pascal Scripts
  3. Add additional classes, functions, and types to Pascal Scripts

Write Pascal Scripts

Here is the script that comes the example.  The script defines the function PrettyPrint and makes use of the custom function print and the custom class TAccumulator.  By custom, I mean, the compiler and runtime had to be extend before print and TAccumulator could be used.

function PrettyPrint(Value: Integer): String;
begin
   Result := 'The value is ' + IntToStr(Value);
end;

var
   Accumulator: TAccumulator;
   I: Integer;
begin
   print('Script started...');
   Accumulator := TAccumulator.Create;
   try
      for I := 3 to 6 do
      begin
         print(PrettyPrint(Accumulator.GetTotal));
         Accumulator.Add(I);
      end;
   finally
      Accumulator.free;
   end;
   print('Script complete.');
end.

Here are some tips that will help you write Pascal Scripts.

  1. Every script must have a main code block; ‘begin … end.’ note the period after end. When the script is run, it starts at the first line in the main code block. Any code that cannot be reached by from the main code block will not be executed. The main code block starts on line 9 in the example.
  2. Functions, constants, and variables that will be used in the script must be defined before the main code block.  Any variable defined outside of a function is global and can be used by any of the functions that it precedes.  The use of global variables is considered bad practice so I like to define them immediately before the main code block.  The variable definitions start on line 6 in the example and can only be used by the main code block.
  3. Functions are supported but procedures are not. This is not a big deal but it can get annoying because the compiler will return a warning if the result of a function is not set.
  4. Scripts can introduce memory leaks.  Any objects created during the scripts execution should also be destroyed.

Compile and Execute Pascal Scripts

The makers of Pascal Script, decide that scripts would be compiled into bytecode before being run.  This means that scripts must be syntactically correct before they can be run (some scripting languages are interpreted line by line during execution).  It also means that bytecode can be reused so a script that is run several times only needs to be compiled once.

Pascal Scripts can be compiled with the class TPSPascalCompiler which is found in the unit uPSCompiler.  On line 12 in the following code, both the Compile and GetOutput methods of TPSPascalCompiler are called.  Both methods return true on success, so if Result is true after line 12 is executed, that means Script was compiled successfully and the resulting bytecode was stored in Bytecode.  The compiler produces both error and warning messages, so lines 13-17 are used to copy any messages into the string Messages.

function TForm1.CompileScript(Script: AnsiString; out Bytecode, Messages: AnsiString): Boolean;
var
    Compiler: TPSPascalCompiler;
    I: Integer;
begin
    Bytecode := '';
    Messages := '';

    Compiler := TPSPascalCompiler.Create;
    Compiler.OnUses := ExtendCompiler;
    try
        Result := Compiler.Compile(Script) and Compiler.GetOutput(Bytecode);
        for I := 0 to Compiler.MsgCount - 1 do
          if Length(Messages) = 0 then
            Messages := Compiler.Msg[I].MessageToString
          else
            Messages := Messages + #13#10 + Compiler.Msg[I].MessageToString;
    finally
        Compiler.Free;
    end;
end;

Once you have the bytecode for your script, you can use TPSExec, found in unit uPSRuntime, to execute it.  Line 10 of the following code attempts to submit Bytecode to the runtime, execute it, and then verify that it executed successfully.  LoadData prepares the runtime to execute Bytecode and returns true if it is ready to run.  RunScript is only called if LoadData returns true and it will return false if the script encounters some sort of runtime error (like divide by 0).  If either method returns false, Runtime.LastEx will contain an error code.  When an error occurs, the function PSErrorToString is used to convert the error code to a string.

function TForm1.RunCompiledScript(Bytecode: AnsiString; out RuntimeErrors: AnsiString): Boolean;
var
  Runtime: TPSExec;
  ClassImporter: TPSRuntimeClassImporter;
begin
  Runtime := TPSExec.Create;
  ClassImporter := TPSRuntimeClassImporter.CreateAndRegister(Runtime, false);
  try
    ExtendRuntime(Runtime, ClassImporter);
    Result := Runtime.LoadData(Bytecode)
          and Runtime.RunScript
          and (Runtime.ExceptionCode = erNoError);
    if not Result then
      RuntimeErrors :=  PSErrorToString(Runtime.LastEx, '');
  finally
    ClassImporter.Free;
    Runtime.Free;
  end;
end;

Extend the scripting language with your own objects, functions, and types

I mentioned earlier that the compiler and runtime have to be extend before print and TAccumulator can be used.  When you extend the compiler, you are telling it that additional classes, functions, and types will be available at runtime.  On the other hand, when you extend the runtime, you provide it with the actual code that will be executed when the additional classes and functions are used by a script.

To extend the compiler, you must assign a function to the OnUses event handler of the compiler object.  I assigned the function ExtendCompiler, shown below, on line 10 of the CompileScript function shown above.  The OnUses event handler is called by the compiler during compilation and will cause compilation to fails if it returns false.  I’m not sure why it is designed this way but trying to extend the compiler from outside the OnUses event handler will result in an exception.

function ExtendCompiler(Compiler: TPSPascalCompiler; const Name: AnsiString): Boolean;
var
    CustomClass: TPSCompileTimeClass;
begin
  try
    Compiler.AddDelphiFunction('procedure print(const AText: AnsiString);');

    SIRegisterTObject(Compiler); // Add compile-time definition for TObject
    CustomClass := Compiler.AddClass(Compiler.FindClass('TObject'), TAccumulator);
    Customclass.RegisterMethod('procedure Add(AValue: Integer)');
    CustomClass.RegisterMethod('function GetTotal: Integer');
    Result := True;
  except
    Result := False; // will halt compilation
  end;
end;

Line 6 of ExtendCompiler passes the definition of the print procedure to the compiler.  If you need to define a type you can use the method AddTypeS which takes the name of the new type and the definition of the new type as strings [i.e. AddTypeS(‘TDynStringArray’, ‘Array of String’)].  When you add functions and types, the compiler will perform compile type checks to make sure they are used correctly (i.e. correct number of arguments passed to a function).

Registering classes is a bit more difficult than registering functions and types.  First you need to register the class which will return an instance of TPSCompileTimeClass.  The instance of TPSCompileTimeClass is then used to register the individual methods.  Line 9, tell the compiler that TAccumulator exists and that it extends the class TObject (FindClass returns an instance of TPSCompileTimeClass for classes that have already been registered).  It is important that the compiler knows TAccumulator extends TObject because the script uses the Free method which is inherited from TObject.

The function SSIRegisterTObject on line 8 registers the class TObject.  The function is defined in the unit uPSC_std which comes with Pascal Script.  There are several units, all starting with uPSC_, that contain functions similar to SSIRegisterTObject that will register common Delphi functions, classes, and types, with the compiler.  I recommend you locate these functions because they will save you a lot of time and code.

Extending the runtime involves assigning function pointers to all of the functions and methods that were added to the compiler.  If this is done incorrectly, i.e. a wrong calling convention is used or a function does not match the declarations given to the compiler, the script will generate errors when run.

procedure TForm1.ExtendRuntime(Runtime: TPSExec; ClassImporter: TPSRuntimeClassImporter);
var
  RuntimeClass: TPSRuntimeClass;
begin
  Runtime.RegisterDelphiMethod(Self, @TForm1.MyPrint, 'print', cdRegister);

  RIRegisterTObject(ClassImporter);
  RuntimeClass := ClassImporter.Add(TAccumulator);
  RuntimeClass.RegisterMethod(@TAccumulator.Add, 'Add');
  RuntimeClass.RegisterMethod(@TAccumulator.GetTotal, 'GetTotal');
end;

The call to RegisterDelphiMethod on line 5 tells the runtime to call the MyPrint method every time the print function is called from the script.  RegisterDelphiMethod was used because my MyPrint is a method and it belongs to a class.  If MyPrint were a function, you would use RegisterDelphiFunction instead which only takes three parameters [i.e. RegisterDelphiFunction(@MyPrint, ‘print’, cdRegister)].

To register classes and their methods with the runtime, an instance of TPSRuntimeClassImporter is needed.  In my example, an instance of  TPSRuntimeClassImporter created on line 7 of RunCompiledScript, passed to the ExtendRuntime function, and then manually freed when the script is finished executing.  I manually free TPSRuntimeClassImporter because the AutoFree option, the second parameter of the constructor, always generates an exception when I try to use it.  Once you have an instance of TPSRuntimeClassImporter, registering classes with the runtime works pretty much the same as registering classes with the compiler.  However, the functions for registering common Delphi functions and classes are stored in units starting with uPSR_.

That’s all I have for now.  Don’t forget to take a look at the example.  It is very short and should be quite helpful.

If you have any questions, suggestions, corrections, etc… please leave a comment.

About these ads

34 Comments

Leave a Comment
  1. Jack / Nov 29 2009 3:18 pm

    Larry,
    Thank you for doing this. It is something that PascalScript has needed for a long time. After working with PascalScript for about a week now, I have accomplished most of what I need to do with it. Like many before me, I will move on to my next task without going back and documenting what I found. You took the time to organize your thoughts and I am sure many will find your post useful. I hope to see it extended and improved over time.

    Jack

  2. Mikaël / Feb 2 2010 6:16 am

    Hi,
    Thanks a lot for this help, I finally found what I need here (IntToStr function)!
    I have to write some Pascal Script but could not find any documentation on the Internet about it. I only found Pascal and understood it was not the same thing (in my case IntToStr function instead of Str function).
    Is there really any documentation giving all the Pascal Script functions?
    Thanks
    Mikaël

  3. Sabu / Jan 22 2011 9:51 am

    Dear Larry,

    I was trying to find a way to work on PascalScript. But I’m not able to start! I have downloaded uPSCompiler.pas but it is not compiled. Can you please help me to successfully install the required units for PascalScripting

  4. lawrencebarsanti / Jan 22 2011 11:14 am

    Try the installer it can be found at http://www.remobjects.com/free.aspx

    I also fixed the link for the example so you can test your installation.

  5. Sabu / Jan 23 2011 1:32 am

    Hi Larry,

    I have downloaded the unit importer. It is fine but I don’t have a clue how to start the scripting! In delphi, I can start with the new application but here how to start , no idea! It would be really great if you can help me in scripting as a beginner. Also I tried to install the package “PascalScript_Core_D5.dpk” that comes along with this but not able as it shows some components missing! In short I’m completely stuck to get a good start!

  6. lawrencebarsanti / Jan 23 2011 11:38 am

    I’m not sure I understand the problem but I hope this helps.

    The purpose of PascalScript is to add scripting support to a program that you are writing. In other words, the application you create is responsible for executing/running the scripts. You only need to use PascalScript if you expect end-users to write scripts that will control the application’s behavior.

  7. Sabu / Jan 24 2011 11:45 am

    Dear Larry,

    That is what I exactly want to do. I want to write a data entry utility where end users can script it according to their needs. I’m trying to use PS but don’t know how to start with the RemObjects Unit importer! Hope you get this!!

  8. lawrencebarsanti / Jan 24 2011 3:10 pm

    The best advice I could give you is to download the
    example, step through it with the debugger, and try to under stand the code blocks posted above. If you have some specific questions about the code, I am happy to answer them.

    Also, keep in mind that this post illustrates how to use PascalScript without the Unit Import tool.

  9. Sabu / Jan 25 2011 12:46 pm

    Hi Larry,

    I’m very much thankful to you for spending time to reply me! However I’m not able to get a start in using PS. Your blog start like ‘I recently started using PS….’ and that is the point where I’m! I want to know what are the steps to be taken to use PS. I downloaded RemObjects Software\Pascal Script for Delphi but unfortunately I’m not able to install any of the packages as they are all Delphi 7+ and I’m still using Delphi 5. Don’t know whether I can use it! I sincerely want an advice on how can I start using it. Once I start using Pascal Script with any application written in Delphi, then I can progress. I know this is purely beyond the scope of your blog, but hope you understand where I stand!

  10. lawrencebarsanti / Jan 25 2011 2:02 pm

    Sorry I don’t know if it works with Delphi 5. I suggest you contact RemObjects support. They have a newsgroup where you could post installation/compatibility questions.

  11. Dave / Nov 1 2011 11:23 am

    Hi Larry,

    Thankyou for this informative blog! I have successfully registered and used delphi functions in Pascal Script using the OnUSES event. I wonder if you know how to programmatically import scripted functions in other units using this event? The compiler has an AllowUnit property and I am hoping to be able to make use of that – although it doesn’t seem to understand interface and implementation sections. I was able to make it compile a simplified unit with effectively just an implied implementation section – but then how do I link the compiled code in the executer?

  12. rhodydog / Nov 5 2011 1:30 pm

    This was an incredibly useful article on how to use the pascal scripting engine. Many thanks for posting it.

  13. brianfrost999Brian Frost / Feb 8 2012 4:06 am

    If you are looking at PascalScript you should take a look at the work done by Eric Grange on DWS which is a high optimised pascal compiler with IDE tools. See http://code.google.com/p/dwscript/

  14. Abner Hernandez / Dec 11 2012 11:05 am

    Hi, Excelent!
    But I have one problem. Maybe I don´t know how to. Of course. Please help me.

    I have a TDataModule named ‘DataPersonal’ with a ClientDataSource named ‘PersonalData’
    and a TFMTBCDField named ‘TotalCost’

    How can I use that field (‘TotalCost’) at compile and runtime pascal script?

    Thanks a lot. Please help me.

  15. Marius / Mar 1 2013 2:37 pm

    @brianfrost: thought new and exciting dwscript is rather poor because is cannot create forms, edit etc. For simple scripts its doing ok

  16. Alfredo / Mar 13 2013 1:51 pm

    Hi, excellent article!
    How I can access to DB?
    Ex.: DBConn := TSQLConnection.create …etc….
    Thanks

  17. lawrencebarsanti / Mar 13 2013 3:16 pm

    @Alfredo, Download the example, run the script, then look at the TAccumulator class. You can follow a similar approach to encapsulate your database connection and only expose specific actions (i.e. listUsers) to the script.

    • Alfredo / Mar 14 2013 12:35 pm

      thanks @lawrence.
      I inserted function ConnectDB on public section of TAccumulator with this code:
      function TAccumulator.ConnectDB: Boolean;
      begin
      try
      DB.DriverName := ‘MSSQL';
      DB.LibraryName := ‘dbxmss.dll';
      DB.VendorLib := ‘sqlncli10.dll';
      DB.GetDriverFunc := ‘getSQLDriverSQLServer';
      DB.Params.Values[‘HostName’] := ‘iphost';
      DB.Params.Values[‘Database’] := ‘dbname';
      DB.Params.Values[‘User_Name’] := ‘sa';
      DB.Params.Values[‘Password’] := ‘thepassword';
      DB.KeepConnection := true;
      DB.LoginPrompt := false;
      DB.Open;
      result := true;
      except
      on e : exception do
      begin
      result := false;
      ShowMessage(e.Message);
      end;
      end;
      end;

      on ExtendCompiler I inserted RegisterMethod(‘function ConnectDB: boolean);

      and in script:
      if Accumulator.ConnectDB then
      print(‘YEAH’)
      else
      print(‘NOOOOOO’);

      but script parser gave me ‘Cannot import’ error.
      What’s wrong?
      Thanks

      • Alfredo / Mar 14 2013 12:49 pm

        Sorry, now on ExtendRuntime I add
        RuntimeClass.RegisterMethod(@TAccumulator.ConnectDB, ‘ConnectDB’);
        and I must create DB object .. now run correctly!!!
        Thanks a lot

  18. Alfredo / Apr 15 2013 3:55 am

    Hi,
    how I can declare and use a record?
    Thanks

  19. Alfredo / Apr 15 2013 11:45 am

    Hi,
    I’ve a record

      TRiga = record
      idRiga : integer;
      TipoRiga: string;
      POD: string;
    end;
    

    on ExtendCompiler
    Compiler.AddType(‘TRiga’,btRecord);
    […]
    Customclass.RegisterProperty(‘Riga’,’TRiga’,iptR);
    and on ExtendRuntime
    RuntimeClass.RegisterPropertyHelper(nil,nil,’Riga’);

    compiler give me error Unknown identifier ‘POD’ when accessing on Riga.POD

    What’s wrong?
    Thanks

    • lawrencebarsanti / Apr 15 2013 3:18 pm

      @Alfredo,

      You can declare and use records in your actual script. For Example

          type
           TCustomRecord = record
            iValue: Integer;
            sValue: String;
           end;
          
          function PrettyPrint(Value: Integer): String;
          begin
             Result := 'The value is ' + IntToStr(Value);
          end;
          
         var
            Rec: TCustomRecord;
         begin
            Rec.iValue := 99;
            Rec.sValue := 'abcd';
            print(PrettyPrint(Rec.iValue));
            print(Rec.sValue);
         end.
      

      From your comments, I believe that you want to extend the compiler so you don’t have to declare the record in your scripts. Unfortunately, it’s not that easy…. `AddType(‘TRiga’,btRecord);` doesn’t works as you would expect. It adds the record type but not its fields – I think it is only meant for internal use.

      If you really want to extend the compiler with your own record type, you have to do something like this.

         var
           RecordType: TPSRecordType;
           FieldDef: PIFPSRecordFieldTypeDef;
         begin
          ...
          RecordType := TPSRecordType(Compiler.AddType('TCustomRecord', btRecord));
      
          FieldDef := RecordType.AddRecVal;
          FieldDef.FieldOrgName := 'iValue';
          FieldDef.aType := Compiler.FindType('Integer');
      
          FieldDef := RecordType.AddRecVal;
          FieldDef.FieldOrgName := 'sValue';
          FieldDef.aType := Compiler.FindType('String');
         end;
      

      Hope this helps

      • Alfredo / Apr 16 2013 3:02 am

        Hi, I written:

        var
          RecordType: TPSRecordType;
          FieldDef: PIFPSRecordFieldTypeDef;
        begin
          try
            RecordType := Compiler.AddType('TRiga',btRecord); // here error incompatible types
            FieldDef := RecordType.AddRecVal;
            FieldDef.FieldName := 'POD'; // error read-only field
            FieldDef.aType := // I don't know what I must put here
        

        Thanks for your time.

    • Lajos Juhasz / Oct 2 2013 8:54 am

      I know that this is an old discussion. In case someone else has a similar problem you can extend the compiler with a new type in the same way as the unit importer does. Thus to introduce your type:

      TRiga = record
      idRiga : integer;
      TipoRiga: string;
      POD: string;
      end;

      It’s enough to add the following line to the PSScriptCompImport method:

      x.AddTypeS(‘TRiga’,’ record idRiga : integer;TipoRiga: string;POD: string; end’);

  20. lawrencebarsanti / Apr 16 2013 5:25 am

    @Alfredo, I’ve updated my original response. I tested the new code with the following script and it works.

    function PrettyPrint(Value: Integer): String;
    begin
       Result := 'The value is ' + IntToStr(Value);
    end;
    
    var
      R: TCustomRecord;
    begin
       R.iValue := 2;
       R.sValue := 'my string';
       print(PrettyPrint(R.iValue));
       print(R.sValue);
    end.
    
    • Alfredo / Apr 16 2013 5:57 am

      Ok, but I can put data in the record from a db query?
      Thanks

  21. Alfredo / Apr 16 2013 5:58 am

    Ok, but how can I put data in the record from a db query?
    Thanks

  22. Magno (@MagnoShell) / May 16 2013 2:41 pm

    Hi people and Thank you Lawrence for such help here!
    Well, this is my 1st attempt to use Script Pascal. I just started by this example. I have just learned about Types, well the TAccumulator class example was much easier to undestand and use :).
    But, there is always one but… I am trying simple things like allow the script to use DateTimeToStr() and it wont work.. Of course what I am wrying is wrong, but I cant figure out how to do…

    Tried that: Runtime.RegisterDelphiFunction(@SysUtils.DateTimeToStr,’DateTimeToStr’,cdRegister);

    • Magno (@MagnoShell) / May 16 2013 3:15 pm

      Well, forget about my last line. Trying to included uPSR_dateutils and in

      RegisterDateTimeLibrary_C(Compiler);

      Discovered that I should call DateToStr(). Well, after try that I got: cannot import

      Thank you for any help

  23. Magno (@MagnoShell) / May 16 2013 4:54 pm

    Everything works great now! Changed uPSR_dateutils.pas to use StrToDateTime() and works fine.

    In pascal, I can call Halt(param) to terminate an application. That statement is not available with ROPS, any alternative?

    Thank you again

  24. Alfredo / Jun 21 2013 8:32 am

    Hi,
    how I can stop execution?
    Thanks

    • Ken Davis / Jan 8 2014 5:43 pm

      You can stop the script from executing by calling pPs.stop

      if (pPs.Running) then pPs.stop;

      This will stop the script engine

      I hope this helps.

  25. snorkel / Jan 9 2014 1:19 pm

    How do you import a whole unit like fileutils?

  26. universe / Sep 19 2014 9:59 am

    Hello, first of all thanks for this nice blog post, it helped a lot to start with adding new types to pascal script. I have an issue with adding an array type (I tried both integer and string and I always receive en exception while compiling)
    The following example is working

    type
    TTest = record
    s:string;
    end;
    RecordType := TPSRecordType(x.AddType('TTest ', btRecord));
    FieldDef := RecordType.AddRecVal;
    FieldDef.FieldOrgName := 's';
    FieldDef.aType := x.FindType('string');

    but this doesn’t

    type
    TTest = record
    s: array of string;
    end;
    RecordType := TPSRecordType(x.AddType('TTest ', btRecord));
    FieldDef := RecordType.AddRecVal;
    FieldDef.FieldOrgName := 's';
    FieldDef.aType := x.FindType('array of string');

    The problem is in FindType(‘array of string’), any advice? Thanks in adavnce.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: