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.AddComponent to register a TComponent instance, or
TatCustomScripter.AddObject 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".
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.DefineClassByRTTI 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
mvPrivateormvProtectedto 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.DefineRecordByRTTI, 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.DefineMethod (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.DefineClass to
register the class, then TatCustomScripter.DefineMethod /
TatCustomScripter.DefineProp 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;
TatCustomScripter.DefineClass registers the class (it checks whether the
class is already registered, so it is safe to call repeatedly).
TatCustomScripter.DefineMethod 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:
- TatVirtualMachine.CurrentObject — the instance the method was called
on (here, the specific
TCustomForm). - TatVirtualMachine.ReturnOutputArg — returns the function result to the script.
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;
Notes:
- Register a method on the highest class that should expose it.
FieldByNameis registered onTDataset, so anyTDatasetdescendant (TTable,TQuery, …) can use it in script. - Use
GetInputArgAsString(0)(orGetInputArg(0),GetInputArg(1), …) to read input parameters by index. - An object result must be cast to
integerwhen passed to TatVirtualMachine.ReturnOutputArg, 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.DefineProp, 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;
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;
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.DefaultProperty. 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.DefineMethod, then in the wrapper call the Delphi method with the correct number of arguments according to TatVirtualMachine.InputArgCount. 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;
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.CurrentMethodName or TatVirtualMachine.CurrentPropertyName:
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;
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.AddConstant 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));
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.DefineProp 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;
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.DefineMethod 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;
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;
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.AddLibrary, or globally with RegisterScripterLibrary:
RegisterScripterLibrary(TExampleLibrary);
RegisterScripterLibrary(TAnotherLibrary, True);
RegisterScripterLibrary 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 ausesclause:uses Another; // use objects and procedures registered by TAnotherLibraryThe name used in the
usesclause is the one returned by the library'sLibraryNameclass 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 exampleMyFontStyle := 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.SystemLibrary:
MyScripter.SystemLibrary.MethodByName('ShowMessage').Free;
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));
}