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:
- Write Pascal Scripts
- Compile and Execute Pascal Scripts
- 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.
- 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.
- 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.
- 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.
- 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.