Table of Contents

Extending Scripter

Out of the box a script can only use the languages and the built-in functions provided by Scripter. To make a script useful it usually needs to interact with your application: read and change properties of your objects, call your methods, use your global functions, constants and variables. Extending Scripter means making these Delphi elements visible to the scripting engine.

There are two ways to expose Delphi classes and members to scripts:

  • Automatic registration using RTTI — the recommended approach. With a single call Scripter inspects a class (or record) using Delphi RTTI and registers all of its members automatically. This is the fastest way to make a class scriptable and is described in Registering classes with RTTI.

  • Manual registration — you declare each method and property individually and provide a small wrapper that performs the actual call. This requires more code, but gives you full control. Use it to fine-tune the integration when RTTI is not enough — for example to expose private members, overloaded methods, methods with default parameters, or to give a member a different name in script. See Manual registration.

Both approaches register members in the TatCustomScripter.​Classes collection and can be freely mixed: you can register a class by RTTI and then add or adjust individual members manually.

Accessing Delphi objects

To use an object in a script, the object must be made known to Scripter by name. Use TatCustomScripter.​Add​Component to register a TComponent instance, or TatCustomScripter.​Add​Object to register any TObject instance. Many Delphi objects are not components, so AddObject is just as important as AddComponent; both take the instance and make it available in script under a name. For example, to allow a script to change the caption of a form named Form1:

Scripter.AddComponent(Form1);

SCRIPT:

Form1.Caption := 'New caption';

AddObject works the same way, but lets you choose the name the object will have in script:

Scripter.AddObject('Application', Application);

Without registering the object first, the script raises "Unknown identifier or variable not declared: Form1".

C++Builder example

Published properties are registered automatically

When you register a component or object, Scripter automatically registers its class and all of its published properties. That is why the form's Caption property could be changed in the example above without any extra code. Public (non-published) properties and methods are not registered automatically — you make them available either with RTTI or manually, as shown in the following sections.

Registering classes with RTTI

Use TatCustomScripter.​Define​Class​ByRTTI to register a class and, in a single call, all of its methods and properties:

Scripter.DefineClassByRTTI(TMyClass);

This is the recommended way to make a class scriptable. It works on every Delphi version supported by Scripter, and replaces the need to declare each member by hand.

You can control exactly what is published using the method's optional parameters:

  • AClassName — a custom name for the class in script; the original class name is used when empty.
  • AVisibilityFilter — registers only members whose visibility is in this set. By default only public and published members are registered; include mvPrivate or mvProtected to register them as well (subject to the limitations below).
  • ARecursive — when True, Scripter also registers the other types (classes, records, enumerated types) used by the methods and properties of the class being defined, using the same visibility filter.

Registering a record

Scripter does not support native records, so a record is registered as if it were a class. Call TatCustomScripter.​Define​Record​ByRTTI, passing the type information of the record:

Scripter.DefineRecordByRTTI(TypeInfo(TRect));

A record registered this way behaves like a class in script and therefore must be instantiated before use (except when a method or property returns the record, in which case Scripter instantiates it automatically):

var
  R: TRect;
begin
  R := TRect.Create;
  try
    R.Left := 100;
    // do something with R
  finally
    R.Free;
  end;
end;
Note

As an alternative to RTTI, a record can be emulated manually with a wrapper class derived from TatRecordWrapper, exposing each record field as a published property. The RTTI method removes the need to write such a wrapper for each record.

Limitations

Because of Delphi RTTI and Scripter constraints, a few members cannot be registered automatically and need a manual workaround:

  • Only members declared in the public and published sections are registered by default. Private and protected methods are not accessible via RTTI even when included in the visibility filter — only their fields and properties are registered.
  • When a method has overloads, only the first declared overload is registered.
  • Methods with default parameter values are registered with all parameters required. To support the default values, register the method manually with TatCustomScripter.​Define​Method (see Default parameters).
  • Event handlers are not registered automatically. Implement a TatEventDispatcher descendant and register it with DefineEventAdapter.
  • Methods with parameters of uncommon types (such as arrays) may be skipped, because Delphi does not provide enough RTTI information for them.

Manual registration (fine-tuning)

Manual registration is the lower-level mechanism that RTTI registration is built upon. Use it when automatic registration is not enough — to expose a member RTTI cannot reach, to rename a member in script, to support default parameters, or to share a single handler across several members.

The pattern is always the same: call TatCustomScripter.​Define​Class to register the class, then TatCustomScripter.​Define​Method / TatCustomScripter.​Define​Prop to register each member, and implement a wrapper — a TatVirtualMachine-aware method that performs the real call. The wrapper receives a TatVirtualMachine that exposes the current object and the input/output arguments.

Calling methods

To register the ShowModal method of a form:

CODE:

procedure TForm1.ShowModalProc(AMachine: TatVirtualMachine);
begin
  with AMachine do
    ReturnOutputArg(TCustomForm(CurrentObject).ShowModal);
end;

procedure TForm1.PrepareScript;
begin
  Scripter.AddComponent(Form2);
  with Scripter.DefineClass(TCustomForm) do
    DefineMethod('ShowModal', 0, tkInteger, nil, ShowModalProc);
end;

SCRIPT:

ShowResult := Form2.ShowModal;

C++Builder example

TatCustomScripter.​Define​Class registers the class (it checks whether the class is already registered, so it is safe to call repeatedly). TatCustomScripter.​Define​Method registers the method; its main arguments are the script name, the number of input arguments, the result data type (tkNone for a procedure), the result class (when the method returns an object), and the wrapper.

Inside the wrapper, the TatVirtualMachine provides everything needed to bridge the call:

You can also register the parameter hint for a method using UpdateParameterHints.

Methods with parameters and class results

This example registers FieldByName, which takes a parameter and returns an object:

SCRIPT:

AField := Table1.FieldByName('CustNo');
ShowMessage(AField.DisplayLabel);

CODE:

procedure TForm1.FieldByNameProc(AMachine: TatVirtualMachine);
begin
  with AMachine do
    ReturnOutputArg(integer(TDataset(CurrentObject).FieldByName(GetInputArgAsString(0))));
end;

procedure TForm1.PrepareScript;
begin
  Scripter.AddComponent(Table1);
  with Scripter.DefineClass(TDataset) do
    DefineMethod('FieldByName', 1, tkClass, TField, FieldByNameProc);
end;

C++Builder example

Notes:

  • Register a method on the highest class that should expose it. FieldByName is registered on TDataset, so any TDataset descendant (TTable, TQuery, …) can use it in script.
  • Use GetInputArgAsString(0) (or GetInputArg(0), GetInputArg(1), …) to read input parameters by index.
  • An object result must be cast to integer when passed to TatVirtualMachine.​Return​OutputArg, because it returns a Variant and objects are passed to script as their instance reference.

Non-published properties

A property that is not published must be registered with TatCustomScripter.​Define​Prop, providing one wrapper to read the value and another to write it. The following registers the Value property of TField:

SCRIPT:

AField := Table1.FieldByName('Company');
ShowMessage(AField.Value);

CODE:

procedure TForm1.GetFieldValueProc(AMachine: TatVirtualMachine);
begin
  with AMachine do
    ReturnOutputArg(TField(CurrentObject).Value);
end;

procedure TForm1.SetFieldValueProc(AMachine: TatVirtualMachine);
begin
  with AMachine do
    TField(CurrentObject).Value := GetInputArg(0);
end;

procedure TForm1.PrepareScript;
begin
  with Scripter.DefineClass(TField) do
    DefineProp('Value', tkVariant, GetFieldValueProc, SetFieldValueProc);
end;

C++Builder example

The tkVariant data type indicates the property is a Variant; the getter uses GetInputArg (which returns a Variant) rather than GetInputArgAsString.

Indexed properties

A property can be indexed — common for TCollection descendants such as dataset fields, grid columns or string lists. The following registers the Strings property of TStrings:

SCRIPT:

ShowMessage(Memo1.Lines.Strings[3]);
Memo1.Lines.Strings[3] := Memo1.Lines.Strings[3] + ' with more text added';

CODE:

procedure TForm1.GetStringsProc(AMachine: TatVirtualMachine);
begin
  with AMachine do
    ReturnOutputArg(TStrings(CurrentObject).Strings[GetArrayIndex(0)]);
end;

procedure TForm1.SetStringsProc(AMachine: TatVirtualMachine);
begin
  with AMachine do
    TStrings(CurrentObject).Strings[GetArrayIndex(0)] := GetInputArgAsString(0);
end;

procedure TForm1.PrepareScript;
begin
  Scripter.AddComponent(Memo1);
  with Scripter.DefineClass(TStrings) do
    DefineProp('Strings', tkString, GetStringsProc, SetStringsProc, nil, false, 1);
end;

C++Builder example

The last three arguments of DefineProp describe the index: the property class (nil, not a class property), whether it is a class property (false), and the number of index parameters (1). For a property indexed by two values, such as a grid's Cells, this last argument would be 2. Inside the wrappers, GetArrayIndex(0) reads the first index.

To make an indexed property the default property of a class, assign the result of DefineProp to TatClass.​Default​Property. With Strings set as the default property of TStrings, the script can use Memo1.Lines[i] instead of Memo1.Lines.Strings[i]:

procedure TForm1.PrepareScript;
begin
  Scripter.AddComponent(Memo1);
  with Scripter.DefineClass(TStrings) do
    DefaultProperty := DefineProp('Strings', tkString,
      GetStringsProc, SetStringsProc, nil, false, 1);
end;

Methods with default parameters

To register a method that has default parameter values, pass the total number of parameters and the number of default parameters to TatCustomScripter.​Define​Method, then in the wrapper call the Delphi method with the correct number of arguments according to TatVirtualMachine.​Input​ArgCount. For a function declared as:

function SumNumbers(A, B: double; C: double = 0; D: double = 0; E: double = 0): double;

register it with five total parameters and three default parameters:

Scripter.DefineMethod('SumNumbers', 5 {total parameters},
  tkFloat, nil, SumNumbersProc, false, 3 {default parameters});

and dispatch on the argument count in the wrapper:

procedure TForm1.SumNumbersProc(AMachine: TatVirtualMachine);
begin
  with AMachine do
    case InputArgCount of
      2: ReturnOutputArg(SumNumbers(GetInputArgAsFloat(0), GetInputArgAsFloat(1)));
      3: ReturnOutputArg(SumNumbers(GetInputArgAsFloat(0), GetInputArgAsFloat(1),
           GetInputArgAsFloat(2)));
      4: ReturnOutputArg(SumNumbers(GetInputArgAsFloat(0), GetInputArgAsFloat(1),
           GetInputArgAsFloat(2), GetInputArgAsFloat(3)));
      5: ReturnOutputArg(SumNumbers(GetInputArgAsFloat(0), GetInputArgAsFloat(1),
           GetInputArgAsFloat(2), GetInputArgAsFloat(3), GetInputArgAsFloat(4)));
    end;
end;

C++Builder example

Sharing a wrapper between members

You can register the same wrapper for more than one method or property. To tell which one was called, use TatVirtualMachine.​Current​Method​Name or TatVirtualMachine.​Current​Property​Name:

procedure TForm1.GenericMessageProc(AMachine: TatVirtualMachine);
begin
  with AMachine do
    if CurrentMethodName = 'MessageHello' then
      ShowMessage('Hello')
    else if CurrentMethodName = 'MessageWorld' then
      ShowMessage('World');
end;

procedure TForm1.PrepareScript;
begin
  with Scripter do
  begin
    DefineMethod('MessageHello', 1, tkNone, nil, GenericMessageProc);
    DefineMethod('MessageWorld', 1, tkNone, nil, GenericMessageProc);
  end;
end;

C++Builder example

Accessing Delphi functions, variables and constants

In addition to objects, Scripter can expose regular procedures and functions, global variables and global constants. Internally, functions and procedures are treated as methods, and variables and constants as properties, so the mechanism is similar to the one for objects.

Registering global constants

Use TatCustomScripter.​Add​Constant with the name the constant will have in script:

CODE:

Scripter.AddConstant('MaxInt', MaxInt);
Scripter.AddConstant('Pi', pi);
Scripter.AddConstant('MyBirthday', EncodeDate(1992, 5, 30));

C++Builder example

SCRIPT:

ShowMessage('Max integer is ' + IntToStr(MaxInt));
ShowMessage('Value of pi is ' + FloatToStr(pi));
ShowMessage('I was born on ' + DateToStr(MyBirthday));

Accessing global variables

Use TatCustomScripter.​Define​Prop with getter/setter wrappers to expose a global variable, giving you full control over how it is read and written. The following registers a ZipCode variable:

CODE:

var
  ZipCode: string[15];

procedure TForm1.GetZipCodeProc(AMachine: TatVirtualMachine);
begin
  with AMachine do
    ReturnOutputArg(ZipCode);
end;

procedure TForm1.SetZipCodeProc(AMachine: TatVirtualMachine);
begin
  with AMachine do
    ZipCode := GetInputArgAsString(0);
end;

procedure TForm1.PrepareScript;
begin
  Scripter.DefineProp('ZipCode', tkString, GetZipCodeProc, SetZipCodeProc);
end;

C++Builder example

SCRIPT:

ShowMessage('Old Zip code is ' + ZipCode);
ZipCode := '109020';

Calling regular functions and procedures

Regular functions and procedures are registered like methods, but added to Scripter itself rather than to a class — call TatCustomScripter.​Define​Method directly. The example below adds QuotedStr and StringOfChar through a library:

SCRIPT:

ShowMessage(QuotedStr(StringOfChar('+', 3)));

CODE:

{ TSomeLibrary }
procedure TSomeLibrary.Init;
begin
  Scripter.DefineMethod('QuotedStr', 1, tkString, nil, QuotedStrProc);
  Scripter.DefineMethod('StringOfChar', 2, tkString, nil, StringOfCharProc);
end;

procedure TSomeLibrary.QuotedStrProc(AMachine: TatVirtualMachine);
begin
  with AMachine do
    ReturnOutputArg(QuotedStr(GetInputArgAsString(0)));
end;

procedure TSomeLibrary.StringOfCharProc(AMachine: TatVirtualMachine);
begin
  with AMachine do
    ReturnOutputArg(StringOfChar(GetInputArgAsString(0)[1], GetInputArgAsInteger(1)));
end;

procedure TForm1.Run1Click(Sender: TObject);
begin
  Scripter.AddLibrary(TSomeLibrary);
  Scripter.SourceCode := Memo1.Lines;
  Scripter.Execute;
end;

C++Builder example

The way methods are defined and wrappers are implemented is exactly the same as before; the only new element is the use of a library to group the registrations, covered next.

Using libraries

A library is an organized way of registering a set of components, classes, methods and properties. Instead of scattering registration code across forms and data modules, you group it in a TatScripterLibrary descendant.

Delphi-based libraries

Compare registering a method directly on a form with registering it through a library:

With a library:

type
  TExampleLibrary = class(TatScripterLibrary)
  protected
    procedure CurrToStrProc(AMachine: TatVirtualMachine);
    procedure Init; override;
    class function LibraryName: string; override;
  end;

class function TExampleLibrary.LibraryName: string;
begin
  result := 'Example';
end;

procedure TExampleLibrary.Init;
begin
  Scripter.DefineMethod('CurrToStr', 1, tkInteger, nil, CurrToStrProc);
end;

procedure TExampleLibrary.CurrToStrProc(AMachine: TatVirtualMachine);
begin
  with AMachine do
    ReturnOutputArg(CurrToStr(GetInputArgAsFloat(0)));
end;

procedure TForm1.Button1Click(Sender: TObject);
begin
  Scripter.AddLibrary(TExampleLibrary);
  Scripter.SourceCode := Memo1.Lines;
  Scripter.Execute;
end;

Directly on the form:

procedure TForm1.PrepareScript;
begin
  Scripter.DefineMethod('CurrToStr', 1, tkInteger, nil, CurrToStrProc);
end;

procedure TForm1.CurrToStrProc(AMachine: TatVirtualMachine);
begin
  with AMachine do
    ReturnOutputArg(CurrToStr(GetInputArgAsFloat(0)));
end;

procedure TForm1.Button1Click(Sender: TObject);
begin
  PrepareScript;
  Scripter.SourceCode := Memo1.Lines;
  Scripter.Execute;
end;

C++Builder example

Both register the same CurrToStr function. The wrapper is identical; the difference is only where the code lives and how it is activated. There is no firm rule on which to use:

  • Registering on an existing object (form, data module) is more convenient — just add a method — but you must ensure the object is instantiated before running the script, and the code is harder to reuse.
  • Using a TatScripterLibrary descendant needs the extra class declaration, but Scripter handles instantiation automatically and the code is easy to port: drop the library into any scripter, move it to a separate unit, and so on.

A library is activated for a single scripter with TatCustomScripter.​Add​Library, or globally with RegisterScripter​Library:

RegisterScripterLibrary(TExampleLibrary);
RegisterScripterLibrary(TAnotherLibrary, True);

RegisterScripter​Library registers the library in a global list so every scripter component is aware of it. The second parameter controls explicit loading:

  • Explicit load False (default) — every scripter instance loads the library automatically.

  • Explicit load True — the library is loaded on demand when a script names it in a uses clause:

      uses Another;
      // use objects and procedures registered by TAnotherLibrary
    

    The name used in the uses clause is the one returned by the library's LibraryName class function.

The System library

TatSystemLibrary is added to every scripter component automatically. It provides the commonly used routines that scripts expect, such as ShowMessage, IntToStr, FormatDateTime, Copy, and many others, mirroring their Delphi counterparts. A few helpers are specific to Scripter:

  • Interpret(AScript: string) — executes the script source code passed as a string.
  • Machine — returns the current TatVirtualMachine.
  • Scripter — returns the current TatCustomScripter.
  • SetOf(array) — builds a set from an array, for example MyFontStyle := SetOf([fsBold, fsItalic]).

Removing functions from the System library

To prevent end-users from calling a particular function, free its method object in the TatCustomScripter.​System​Library:

MyScripter.SystemLibrary.MethodByName('ShowMessage').Free;

C++Builder example

The VBScript library

TatVBScriptLibrary adds many VBScript-compatible functions (MsgBox, InStr, CDbl, FormatNumber, UCase, and so on), making it easier for users familiar with VBScript to write Basic scripts. Unlike the System library it is not added automatically. Add it from Delphi code:

Scripter.AddLibrary(TatVBScriptLibrary);

or enable it from the script itself through the uses clause:

'My Basic Script
uses VBScript

C++Builder integration

Because Scripter relies heavily on object types and typecasting, a few tasks need extra care in C++Builder. The C++Builder Examples chapter mirrors the Delphi examples in this guide. One C++-specific case is registering a class constructor as a class method.

Given a class testclass derived from TObject, register a Create class method so scripts can instantiate it:

scr->DefineMethod("create", 0, Typinfo::tkClass, __classid(testclass), constProc, true);

and implement the wrapper that performs the construction:

void __fastcall TForm1::constProc(TatVirtualMachine* avm)
{
  testclass *l_tc;

  l_tc = (testclass *) avm->CurrentObject;
  l_tc = new testclass;
  avm->ReturnOutputArg((long)(l_tc));
}